Skip to content
Browse files

initial public release

  • Loading branch information...
1 parent 536954d commit b0bb1ad9f2f3d39dd9c56779cbd5092bc120367a @chair6 chair6 committed Aug 5, 2012
View
8 .gitignore
@@ -0,0 +1,8 @@
+*.pyc
+*.class
+*.db
+*.xlsx
+.DS_Store
+*.bak
+
+burpsuite_*.jar
View
15 CREDITS.md
@@ -0,0 +1,15 @@
+Credits
+=======
+
+* Hiccup wouldn't exist, and application security testing would be significantly less pleasant, if it wasn't for Burp Suite (http://portswigger.net) by Dafydd Stuttard.
+
+* Based on initial work by David Robert (http://blog.ombrepixel.com/post/2010/08/30/Extending-Burp-Suite-in-Python).
+
+* Uses Jython (http://jython.org) 2.5.3b with additional libraries:
+ - OpenPyXL (http://packages.python.org/openpyxl/)
+ - PyYaml (http://pyyaml.org/)
+ - json.py (http://sourceforge.net/projects/json-py/)
+ - PyAMF (http://www.pyamf.org/)
+
+* Includes SQLiteJDBC (http://www.zentus.com/sqlitejdbc/).
+
View
20 LICENSE.md
@@ -0,0 +1,20 @@
+Copyright (c) 2012 Zynga Inc. http://zynga.com/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
99 README.md
@@ -1,4 +1,97 @@
-hiccup
-======
+Hiccup - Burp Suite Python Extensions
+=====================================
-Hiccup is a framework that allows the Burp Suite (a web application security testing tool, http://portswigger.net/burp/) to be extended and customized, through the interface provided by Burp Extender (http://portswigger.net/burp/extender/). Its aim is to allow for the development and integration of custom testing functionality into the Burp tool using Python request/response handler plugins.
+Hiccup is a framework that allows the Burp Suite (a web application security testing tool, [http://portswigger.net/burp/](http://portswigger.net/burp/)) to be extended and customized, through the interface provided by Burp Extender ([http://portswigger.net/burp/extender/](http://portswigger.net/burp/extender/)). Its aim is to allow for the development and integration of custom testing functionality into the Burp tool using Python request/response handler plugins.
+
+
+Installing and Using Hiccup
+---------------------------
+
+1. Clone/unzip this repository/file into its own directory, retaining the existing subdirectory structure.
+2. Copy a Burp Suite JAR file (free or professional, see [http://portswigger.net/burp/download.html](http://portswigger.net/burp/download.html)) into this same directory.
+3. Review the hiccup.yaml configuration file and make any immediate changes that might be necessary.
+4. From a shell or window, run the hiccup.sh (Mac OS X or Linux) or hiccup.bat (Windows) file, and start using Burp. Watch the terminal/DOS window for confirmation that Hiccup has initialized and for output from the plugins.
+5. Use web browser and Burp as usual. Hiccup plugin output will appear in the terminal/DOS window.
+6. Move plugins in and out of the plugins/ directory to enable or disable them (plugins located in the plugins/disabled/ directory, or other sub-directories, will not be loaded). Modify, or create new plugins as necessary. Movements and edits to plugins are detected and reloaded automatically.
+
+
+Extensions Framework
+--------------------
+
+The framework is made up of components as follow:
+
+* hiccup/hiccup.yaml
+
+ Base configuration file for the Hiccup framework. The YAML file contains global settings along with plugin-specific configuration items, and is easily extended with new sections added as required for additional plugins. Hiccup will detect any changes to the configuration file during runtime and reload as required.
+
+* hiccup/BurpExtender.py
+
+ Main interface from Hiccup to Burp Extender. Defines the processProxyMessage and processHttpMessage functions, which create the Message object with the request/response data and passes it to the PluginManager for handling. It also registers the Burp Extender callbacks object (through the registerExtenderCallbacks function) which exposes all IBurpExtenderCallbacks functions ([http://portswigger.net/burp/extender/burp/IBurpExtenderCallbacks.html](http://portswigger.net/burp/extender/burp/IBurpExtenderCallbacks.html)), and initializes GlobalConfig and PluginManager at load time.
+
+* hiccup/MenuItemHandler.py
+
+ Secondary interface from Hiccup to Burp Extender. Defines the menuItemClicked function, which creates an array of Message objects with associated request/response data and passes it to the PluginManager for handling.
+
+* hiccup/Message.py
+
+ The object that stores various data associated with the request/response being processed. Data values stored include ref, method, url, headers (raw and parsed), body, contenttype, resourcetype, statuscode, remotehost, and remoteport. Various helper methods are provided to aid in ease of plugin development, and additional helper methods can be added to the base object as necessary.
+
+ Plugins make their changes directly to this object as processing occurs.
+
+* hiccup/PluginManager.py
+
+ Locates, loads, reloads, and unloads plugins according to file existence/changes as monitored by FileWatcher. Pushes messages from Burp Extender through to individual plugins.
+
+ See plugins/README.md for detailed documentation.
+
+* hiccup/FileWatcher.py
+
+ Generic file change tracker, used by various Hiccup elements to detect changes to files.
+
+* hiccup/GlobalConfig.py
+
+ Defines configuration options and state-related objects that are used globally by Hiccup. A GlobalConfig object is initialized when Hiccup is first loaded, and is passed to each plugin for reference as appropriate.
+
+ Configuration is read in from the YAML configuration file, hiccup.yaml.
+
+* hiccup/SharedFunctions.py
+
+ Stores generic shared functions.
+
+* hiccup/BasePlugin.py
+
+ The base class from which plugins should be defined as subclasses.
+
+* plugins/
+
+ Holds Python request/response handler plugins.
+
+ The plugins directory will be automatically monitored by the PluginManager, which will load, unload, and reload plugins automatically (as they are moved into or out of the directory, or as changed are detected to individual plugin files).
+
+ See plugins/README.md for detailed documentation.
+
+* plugins/disabled/
+
+ Plugins that should not be loaded by PluginManager are stored in this directory (or other sub-directories that are created).
+
+
+Plugins
+-------
+
+See plugins/README.md.
+
+
+Authors
+=======
+Hiccup was developed by Jamie Finnigan (http://twitter.com/chair6), based on initial work by David Robert (http://blog.ombrepixel.com/post/2010/08/30/Extending-Burp-Suite-in-Python), and is continued as an official Zynga OpenSource (http://code.zynga.com/) project.
+
+
+License
+=======
+Copyright (c) 2012 Zynga Inc. http://zynga.com/
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
49 hiccup.bat
@@ -0,0 +1,49 @@
+@echo off
+
+set COUNT=0
+
+set STARTDIR=%CD%\
+set BATDIR=%~dp0
+cd /D %BATDIR%
+
+for /f "tokens=*" %%a in (
+'forfiles /P %BATDIR% /M burpsuite_*.jar /C "cmd /c echo @fname" ^| find /C "burpsuite"'
+) do (
+set COUNT=%%a
+)
+
+
+if %COUNT%==1 goto ONE
+if %COUNT% gtr 1 goto MANY
+goto NONE
+
+:ONE
+for %%X in (burpsuite_*.jar) do set BURP_PACKAGE=%%X
+goto EXECBURP
+
+:MANY
+for %%X in (burpsuite_*.jar) do (
+ choice /C:ny /M "Found %COUNT% Burp packages; use %%X"
+ if ERRORLEVEL 2 (
+ set BURP_PACKAGE=%%X
+ goto EXECBURP
+ )
+)
+goto NONE
+
+
+:NONE
+echo ERROR: A necessary Burp Suite package could not be located, or was not selected; download from http://portswigger.net/burp/download.html, or view README file for more instructions.
+cd /D %STARTDIR%
+pause
+goto EOF
+
+:EXECBURP
+echo Initializing Burp with package %BURP_PACKAGE%...
+java -Xmx1024m -classpath %BURP_PACKAGE%;lib\BurpExtender.jar;lib\sqlitejdbc-v056.jar -Dpython.path=%CD%;%CD%\lib;%CD%\hiccup;%CD%\plugins burp.StartBurp
+goto EOF
+
+
+:EOF
+cd %STARTDIR%
+exit /B
View
28 hiccup.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+BURP_PACKAGE="";
+
+BURP_COUNT=`ls burpsuite_*.jar | wc -l`;
+
+if [ $BURP_COUNT = 1 ]; then
+ BURP_PACKAGE=`ls burpsuite_*.jar`;
+else
+ for i in ls burpsuite_*.jar; do
+ if [ -e ${i} ]; then
+ echo "Found ${BURP_COUNT#"${BURP_COUNT%%[![:space:]]*}"} Burp packages; use ${i}? (y/n): \c"
+ read answer
+ case ${answer} in
+ y*|Y*) BURP_PACKAGE=${i}; break; ;;
+ n*|N*) ;;
+ *) : ;;
+ esac
+ fi
+ done
+fi
+
+if [ "x$BURP_PACKAGE" == "x" ]; then
+ echo "ERROR: A necessary Burp Suite package could not be located, or was not selected; view README file, or download from http://portswigger.net/burp/download.html.";
+ exit 1;
+fi
+
+java -Xmx1024m -classpath $BURP_PACKAGE:lib/BurpExtender.jar:lib/sqlitejdbc-v056.jar -Dpython.path=$PWD:$PWD/lib:$PWD/hiccup:$PWD/plugins burp.StartBurp
View
40 hiccup.yaml
@@ -0,0 +1,40 @@
+####################
+defaults:
+ default_plugin_scope: proxy_only #all, proxy_only, or http_only
+ intercept_enabled: False #automatically enable/disable intercepts on load - True or False
+ log_format: '[%(module)s] %(message)s'
+ log_level: info #info or debug
+ plugin_directory: plugins
+ auto_delete_class_files: True #True or False - autodelete stale class files for disabled plugins
+ default_intercept_action: FOLLOW_RULES
+ #burp_scope_include: ['http://www.google.com',] #must be at least valid scheme://host URL
+ #burp_scope_exclude: ['http://www.microsoft.com',]
+internals: [] # reserved for internal Hiccup use
+state: [] # reserved for internal Hiccup use
+callbacks: [] # reserved for internal Hiccup use
+####################
+ConditionalIntercepts:
+ body_contains: ['internal only']
+ header_exists: [x-hacker] #triggered by wordpress.com
+ headers_contain: ['DROP TABLE '] #triggered by reddit.com
+ host_contains: [google.com]
+ url_contains: [admin]
+ContentReplace:
+ targets:
+ - [' the ', ' FOO ']
+ - [' and ', ' MEH ']
+CookieProfiler:
+ output_file: cookie-summary.xlsx
+ write_after: 20
+DatabaseLogger:
+ storable_types: [ 'text/','application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'application/x-www-form-urlencoded' ]
+ output_file: logger.db
+DropMatches:
+ domains: [domaintodrop.com, otherdomaintodrop.com]
+ hosts: [specific.hostname.com, specific2.hostname.com]
+HeaderReplace:
+ targets:
+ - ['Firefox', 'FireDUCK']
+JsonManipulation:
+ delete_keys: [about] #test against jsonip.com
+ set_values: {ip: foo, nokey: noworries } #test against jsonip.com
View
51 hiccup/BasePlugin.py
@@ -0,0 +1,51 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+import SharedFunctions as shared
+import logging
+
+class BasePlugin:
+
+ required_config = []
+ config_complete = False
+ plugin_name = None
+ logger = None
+ plugin_scope = None
+
+ plugin_scopes = {
+ 'all': 0,
+ 'proxy_only': 1,
+ 'http_only': 2
+ }
+
+ def __init__(self, global_config, reqdconf=[], plugin_scope=None):
+ self.logger = logging.getLogger()
+ self.logger.debug("initializing '%s', required_config %s" % (self.__module__, reqdconf))
+ self.global_config = global_config
+ self.plugin_name = self.__module__
+ if plugin_scope == None:
+ plugin_scope = global_config['defaults']['default_plugin_scope']
+ self.logger.debug("plugin did not provide scope, using default: %s" % plugin_scope)
+ elif plugin_scope not in self.plugin_scopes:
+ self.logger.error("plugin provided invalid scope '%s', using default '%s'" % (plugin_scope, global_config['defaults']['default_plugin_scope']))
+ plugin_scope = global_config['defaults']['default_plugin_scope']
+ self.plugin_scope = plugin_scope
+ self.required_config = reqdconf
+ if self.global_config.test_plugin_config(self.plugin_name, reqdconf):
+ self.config_complete = True
+ self.logger.debug("'%s' plugin initialized" % (self.__module__))
+
+ def __del__(self):
+ pass
+
+ def required_config_loaded(self):
+ return self.config_complete
+
+ def scope_proxy_only(self):
+ return self.plugin_scopes[self.plugin_scope] == self.plugin_scopes['proxy_only']
+
+ def scope_http_only(self):
+ return self.plugin_scopes[self.plugin_scope] == self.plugin_scopes['http_only']
+
+ def scope_all(self):
+ return self.plugin_scopes[self.plugin_scope] == self.plugin_scopes['all']
View
223 hiccup/BurpExtender.py
@@ -0,0 +1,223 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from burp import IBurpExtender
+import sys, os, re, time, logging
+
+from hiccup import GlobalConfig, PluginManager, FileWatcher, Message, MenuItemHandler
+from hiccup import SharedFunctions as shared
+
+class BurpExtender(IBurpExtender):
+
+ config_file = 'hiccup.yaml'
+ conf_watcher = None
+
+ init_level = 0 #0 at launch, 1 with fwatcher, 2 with config, 3 with callbacks, 4 with pluginmanager
+ init_sleep = 3 #seconds to wait before retesting for callbacks
+ file_watcher = None
+ global_config = None
+ plugin_manager = None
+ callbacks = None
+
+ logger = None
+ handler = None
+ log_level = logging.INFO #default - logging.INFO or logging.DEBUG
+ log_format = '[%(module)s] %(message)s'
+
+ ######
+ ### INIT FUNCTIONS
+ ######
+ def __init__(self):
+ self.__init_logger()
+ self.__init_filewatcher()
+ self.__init_config()
+
+ def __init_logger(self):
+ if self.logger == None:
+ self.logger = logging.getLogger()
+ if self.global_config != None:
+ try:
+ self.log_level = logging.DEBUG if self.global_config['defaults']['log_level'] == 'debug' else logging.INFO
+ except TypeError, e:
+ self.logger.error("config file does not define global -> log_level, using default")
+ try:
+ self.log_format = self.global_config['defaults']['log_format']
+ except TypeError, e:
+ self.logger.error("config file does not define global -> log_format, using default")
+ self.logger.setLevel(self.log_level)
+ if self.handler == None:
+ self.handler = logging.StreamHandler(sys.stdout)
+ self.handler.setFormatter(logging.Formatter(self.log_format))
+ self.logger.addHandler(self.handler)
+
+ def __init_filewatcher(self):
+ try:
+ self.file_watcher = FileWatcher.FileWatcher('hiccup', ['GlobalConfig.py', 'PluginManager.py', 'SharedFunctions.py', 'zSharedFunctions.py', 'Message.py', 'BasePlugin.py', 'MenuItemHandler.py'])
+ except Exception, e:
+ self.logger.error("exception initializing FileWatcher : %s" % (e))
+ else:
+ self.__change_init(1)
+
+ def __init_config(self):
+ if os.path.isfile(self.config_file) == False:
+ self.logger.error("Configuration file '%s' not found." % (os.path.join(os.getcwd(), self.config_file)))
+ else:
+ self.global_config = GlobalConfig.GlobalConfig(self.config_file)
+ self.conf_watcher = FileWatcher.FileWatcher('.', [self.config_file,])
+ if self.global_config.is_valid():
+ self.__init_logger()
+ self.__change_init(2)
+ else:
+ self.__change_init(1, True)
+
+ def __init_callbacks(self, callbacks):
+ if self.init_level > 1:
+ try:
+ self.callbacks = callbacks
+ self.global_config.add_callbacks(callbacks)
+ except Exception, e:
+ self.logger.error("exception initializing callbacks : %s" % (e))
+ self.callbacks = None
+ else:
+ self.__change_init(3)
+ self.__init_menuhandler()
+ self.__init_pluginmanager()
+
+ def __init_pluginmanager(self):
+ while self.init_level != 3:
+ self.logger.info("waiting for Burp to finish initializing environment")
+ time.sleep(self.init_sleep)
+ try:
+ self.logger.debug("starting PluginManager")
+ self.plugin_manager = PluginManager.PluginManager(self.global_config)
+ self.global_config['internals']['menu_handler'].set_plugin_manager(self.plugin_manager)
+ except Exception, e:
+ self.logger.error("exception initializing PluginManager : %s" % (e))
+ else:
+ self.__change_init(4, True)
+
+ def __init_menuhandler(self):
+ self.global_config['internals']['menu_handler'] = MenuItemHandler.MenuItemHandler(self.global_config, self.logger, self.plugin_manager, self)
+
+ def __change_init(self, level, notify=False):
+ if level == 1:
+ self.logger.debug("switching to init_level 1")
+ if notify: self.logger.info("Burp will proxy messages but they will not be processed by Hiccup")
+ elif level == 2:
+ self.logger.debug("switching to init_level 2")
+ if notify: self.logger.info("Burp will proxy messages but they will not be processed by Hiccup")
+ elif level == 3:
+ self.logger.debug("switching to init_level 3")
+ if notify: self.logger.info("Burp will proxy messages but they will not be processed by Hiccup")
+ elif level == 4:
+ self.logger.debug("switching to init_level 4")
+ if notify: self.logger.info("Hiccup initialized")
+ else:
+ self.logger.error("__change_init to unrecognized init_level: %s" % level)
+
+ self.init_level = level
+
+ ### BURP FUNCTIONS
+ #registerExtenderCallbacks called on startup to register callbacks object
+ def registerExtenderCallbacks(self, callbacks):
+ self.logger.debug("registerExtenderCallbacks received call (init_level:%s)" % (self.init_level))
+ self.__init_callbacks(callbacks)
+
+ ## processHttpMessage called whenever any of Burp's tools makes an HTTP request or receives a response
+ ## - for requests, involved immediately before request sent to network
+ ## - for responses, invoked immediately after request is received from network
+ def processHttpMessage(self, toolName, messageIsRequest, messageInfo):
+ self.reload_on_change()
+ if self.init_level == 4:
+ messageType = toolName
+ messageReference = '~'
+ remoteHost = messageInfo.getHost()
+ remotePort = messageInfo.getPort()
+ serviceIsHttps = True if messageInfo.getProtocol() == 'https' else False
+ httpMethod = ''
+ url = '%s://%s%s' % (messageInfo.getUrl().getProtocol(), messageInfo.getUrl().getHost(), messageInfo.getUrl().getPath())
+ resourceType = ''
+ statusCode = '' if messageIsRequest else messageInfo.getStatusCode()
+ responseContentType = ''
+ messageRaw = messageInfo.getRequest() if messageIsRequest else messageInfo.getResponse()
+ interceptAction = ['',]
+ message = Message.Message(self.global_config, messageType, messageReference, messageIsRequest,
+ remoteHost, remotePort, serviceIsHttps, httpMethod, url, resourceType,
+ statusCode, responseContentType, messageRaw, interceptAction)
+ self.__process_message(message)
+ if message.is_changed():
+ message.update_content_length()
+ messageInfo.setRequest(message['headers'] + message['body']) if messageIsRequest else messageInfo.setResponse(message['headers'] + message['body'])
+ if message.is_highlighted():
+ messageInfo.setHighlight(message.get_highlight())
+ if message.is_commented():
+ messageInfo.setComment(message.get_comment())
+
+ ## processProxyMessage method, called by Burp when a message is passed through the proxy.
+ def processProxyMessage(self, messageReference, messageIsRequest, remoteHost, remotePort,
+ serviceIsHttps, httpMethod, url, resourceType, statusCode,
+ responseContentType, messageRaw, interceptAction):
+ self.reload_on_change()
+ if self.init_level == 4:
+ messageType = 'proxy'
+ message = Message.Message(self.global_config, messageType, messageReference, messageIsRequest,
+ remoteHost, remotePort, serviceIsHttps, httpMethod, url,
+ resourceType, statusCode, responseContentType, messageRaw, interceptAction)
+ self.__process_message(message)
+ interceptAction[0] = message['interceptaction']
+ if message.is_changed() == False:
+ return message['raw']
+ else:
+ message.update_content_length()
+ return message['headers'] + message['body']
+
+ ## applicationClosing method, called by Burp immediately before exit
+ def applicationClosing(self):
+ self.logger.info("Hiccup shutting down during Burp exit")
+ if (self.global_config['defaults']['auto_delete_class_files'] == True):
+ for fname in os.listdir(self.global_config['defaults']['plugin_directory']):
+ if (fname.endswith('$py.class')):
+ self.logger.debug("deleting stale .class file : %s" % fname)
+ os.remove(os.path.join(self.global_config['defaults']['plugin_directory'], fname))
+ ######
+ ### INTERNAL FUNCTIONS
+ ######
+
+ # run message (request/response) through plugins via plugin_manager
+ def __process_message(self, message):
+ if (message.is_request()):
+ self.plugin_manager.process_request(message)
+ else:
+ self.plugin_manager.process_response(message)
+
+ # do config/module/plugin reloads, if changes detected
+ def reload_on_change(self):
+ self.logger.debug("testing for config/module/plugin changes")
+ if len(self.conf_watcher.get_changed()) > 0:
+ self.logger.info("configuration file change detected, reloading")
+ self.global_config.reload_from_file()
+ if self.global_config.is_valid() == False:
+ self.__change_init(1, True)
+ else:
+ self.plugin_manager = PluginManager.PluginManager(self.global_config)
+ self.global_config['internals']['menu_handler'].set_plugin_manager(self.plugin_manager)
+ self.__init_logger()
+ self.__change_init(4, True)
+ if self.init_level > 2:
+ for fname in self.file_watcher.get_changed():
+ modname = ''.join(fname.split('.')[:-1])
+ self.logger.info(" module change detected, reloading '%s'" % (modname))
+ if modname == 'BasePlugin':
+ self.plugin_manager = PluginManager.PluginManager(self.global_config)
+ self.global_config['internals']['menu_handler'].set_plugin_manager(self.plugin_manager)
+ else:
+ reload(sys.modules["hiccup." + modname])
+ if (modname == 'GlobalConfig'):
+ self.global_config = GlobalConfig.GlobalConfig(self.config_file, self.callbacks)
+ self.plugin_manager = PluginManager.PluginManager(self.global_config)
+ self.global_config['internals']['menu_handler'].set_plugin_manager(self.plugin_manager)
+ elif (modname == 'PluginManager'):
+ self.plugin_manager = PluginManager.PluginManager(self.global_config)
+ self.global_config['internals']['menu_handler'].set_plugin_manager(self.plugin_manager)
+ if self.init_level == 4:
+ self.plugin_manager.reload_changed()
View
45 hiccup/FileWatcher.py
@@ -0,0 +1,45 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+import os
+import logging
+
+class FileWatcher:
+
+ def __init__(self, dname, fnames):
+ self.dname = dname
+ self.fnames = {}
+ self.logger = logging.getLogger()
+ for fname in fnames:
+ fstat = self.__fstat(self.dname, "%s" % fname)
+ if fstat != False:
+ self.fnames[fname] = fstat
+ self.logger.debug("initialized (%s : %s)" % (self.dname, self.fnames.keys()))
+
+ def get_changed(self):
+ changed = []
+ for (fname,v) in self.fnames.iteritems():
+ tmpv = self.__fstat(self.dname, "%s" % fname)
+ if (tmpv != False and tmpv != v):
+ self.logger.debug("change detected in %s" % (os.path.join(self.dname, "%s" % fname)))
+ self.fnames[fname] = tmpv
+ changed.append(fname)
+ return changed
+
+ def remove_item(self, fname):
+ if (fname in self.fnames.keys()):
+ del(self.fnames[fname])
+
+ def add_item(self, fname):
+ if (fname not in self.fnames.keys()):
+ fstat = self.__fstat(self.dname, "%s" % fname)
+ if (fstat != False):
+ self.fnames[fname] = fstat
+
+ def __fstat(self, dname, fname):
+ try:
+ fstat = os.stat(os.path.join(dname, fname)).st_mtime
+ except OSError, e:
+ return False
+ else:
+ return fstat
View
133 hiccup/GlobalConfig.py
@@ -0,0 +1,133 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+import FileWatcher
+import os, ConfigParser, logging, json, yaml
+from java.net import URL
+
+class GlobalConfig:
+ base_section = 'defaults'
+ logger = None
+ filename = ''
+ file_watcher = None
+ config_data = {}
+ config_internals = {
+ 'intercept_actions': {
+ 'FOLLOW_RULES': 0, 'DO_INTERCEPT': 1,'DONT_INTERCEPT': 2, 'DROP': 3,
+ 'FOLLOW_RULES_AND_REHOOK': 0x10, 'DO_INTERCEPT_AND_REHOOK': 0x11, 'DONT_INTERCEPT_AND_REHOOK': 0x12
+ },
+ 'menu_handler': {},
+ 'handler_map': {}
+ }
+ config_state = {}
+ config_callbacks = None
+ valid = False
+
+ def __init__(self, configfile, callbacks=None):
+ self.logger = logging.getLogger()
+ self.filename = configfile
+ self.config_data = self.load_from_file(configfile)
+ self.config_data['internals'] = self.config_internals
+ self.config_data['state'] = self.config_state
+ if callbacks != None:
+ self.add_callbacks(callbacks)
+ self.logger.debug("initialized")
+
+ def add_callbacks(self, callbacks):
+ self.config_callbacks = callbacks
+ self.config_data['callbacks'] = self.config_callbacks
+ #do burp-specific config according to config file
+ if self.base_section in self.config_data:
+ if 'intercept_enabled' in self.config_data[self.base_section]:
+ if self.config_data[self.base_section]['intercept_enabled']:
+ self.config_callbacks.setProxyInterceptionEnabled(True)
+ else:
+ self.config_callbacks.setProxyInterceptionEnabled(False)
+ if 'burp_scope_include' in self.config_data[self.base_section]:
+ for item in self.config_data[self.base_section]['burp_scope_include']:
+ self.logger.debug("adding '%s' to Burp scope" % item)
+ self.config_data['callbacks'].includeInScope(URL(item))
+ if 'burp_scope_exclude' in self.config_data[self.base_section]:
+ for item in self.config_data[self.base_section]['burp_scope_exclude']:
+ self.logger.debug("excluding '%s' from Burp scope" % item)
+ self.config_data['callbacks'].excludeFromScope(URL(item))
+ #tweak other burp-specific settings
+ tmpconf = self['callbacks'].saveConfig()
+ for name in ('proxy', 'target'):
+ for mimetype in ('html', 'script', 'xml', 'css', 'othertext', 'images', 'flash', 'otherbinary'):
+ tmpconf['%s.showmime%s' % (name, mimetype)] = 'true'
+ for status in ('2xx', '3xx', '4xx', '5xx'):
+ tmpconf['%s.showstatus%s' % (name, status)] = 'true'
+ tmpconf['proxy.interceptresponses'] = 'true'
+ tmpconf['proxy.listener0'] = '1.8080.0.0..0.0.1.0..0..0.'
+ self['callbacks'].loadConfig(tmpconf)
+ self.logger.debug("added callbacks")
+
+ def reload_from_file(self):
+ self.logger.debug("reload_from_file() called")
+ self.config_state = self.config_data['state']
+ self.config_internals = self.config_data['internals']
+ self.config_data = self.load_from_file(self.filename)
+ self.config_data['internals'] = self.config_internals
+ self.config_data['state'] = self.config_state
+ self.config_data['callbacks'] = self.config_callbacks
+
+ def test_plugin_config(self, pname, reqdconfitems):
+ if len(reqdconfitems) == 0:
+ return True
+ if pname not in self.config_data:
+ return False
+ for item in reqdconfitems:
+ if item not in self.config_data[pname]:
+ return False
+ return True
+
+ def load_from_file(self, filename):
+ self.valid = False
+ if (os.path.isfile(filename)):
+ try:
+ cfgfile = open(filename, 'r')
+ configobj = yaml.load(cfgfile.read())
+ self.logger.debug("config read from file: %s" % configobj)
+ except Exception, e:
+ self.logger.error("exception reading config file: %s" % e)
+ return {}
+ else:
+ self.valid = True
+ return configobj
+ cfgfile.close()
+ else:
+ self.logger.error("config file '%s' not found" % os.path.join(os.getcwd(), filename))
+ return {}
+
+ def is_valid(self):
+ return self.valid
+
+ def register_menuitem(self, caption, plugin_name):
+ if caption in self['internals']['handler_map']:
+ self.logger.error("menu item '%s' already registered for plugin '%s'" % (caption, self['internals']['handler_map'][caption]))
+ else:
+ self['callbacks'].registerMenuItem(caption, self['internals']['menu_handler'])
+ self['internals']['handler_map'][caption] = plugin_name
+ self.logger.debug("register_menuitem() added '%s' to map for '%s'" % (caption, plugin_name))
+
+ #act like a dict
+ def __getitem__(self, key):
+ if key in self.config_data:
+ return self.config_data[key]
+ else:
+ raise KeyError("'%s' not found in configuration" % key)
+
+ def __setitem__(self, key, value):
+ self.config_data[key] = value
+
+ def __iter__(self):
+ return self.config_data.iterkeys()
+
+ def iterkeys(self):
+ return self.config_data.iterkeys()
+
+ def __contains(self, key):
+ if key in self.config_data:
+ return True
+ return False
View
46 hiccup/MenuItemHandler.py
@@ -0,0 +1,46 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from burp import IMenuItemHandler
+import sys, os, re, time, logging
+
+import GlobalConfig, Message
+import SharedFunctions as shared
+
+class MenuItemHandler(IMenuItemHandler):
+
+ def __init__(self, config, logger, mgr, hiccup):
+ self.global_config = config
+ self.logger = logger
+ self.plugin_manager = mgr
+ self.hiccup = hiccup
+
+ ## menuItemClicked method, called by Burp when the user clicks a custom menu item.
+ def menuItemClicked(self, menuItemCaption, selectedMessages):
+ self.hiccup.reload_on_change()
+ self.logger.debug("menuItemClicked : %s" % menuItemCaption)
+ messages = []
+ for messageInfo in selectedMessages:
+ self.logger.debug("menuItemClicked() messageInfo : %s" % messageInfo)
+ messageReference = '~'
+ remoteHost = messageInfo.getHost()
+ remotePort = messageInfo.getPort()
+ serviceIsHttps = True if messageInfo.getProtocol() == 'https' else False
+ httpMethod = ''
+ url = '%s://%s%s' % (messageInfo.getUrl().getProtocol(), messageInfo.getUrl().getHost(), messageInfo.getUrl().getPath())
+ resourceType = ''
+ responseContentType = ''
+ interceptAction = ['',]
+ #deal with request
+ messages.append(Message.Message(self.global_config, 'clicked', messageReference, True,
+ remoteHost, remotePort, serviceIsHttps, httpMethod, url, resourceType,
+ '', responseContentType, messageInfo.getRequest(), interceptAction))
+ #and response
+ messages.append(Message.Message(self.global_config, 'clicked', messageReference, False,
+ remoteHost, remotePort, serviceIsHttps, httpMethod, url, resourceType,
+ messageInfo.getStatusCode(), responseContentType, messageInfo.getResponse(), interceptAction))
+ self.plugin_manager.process_menuitem_click(menuItemCaption, messages)
+
+ def set_plugin_manager(self, mgr):
+ self.plugin_manager = mgr
+
View
237 hiccup/Message.py
@@ -0,0 +1,237 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import SharedFunctions as shared
+from java.net import URL
+
+import re, logging, urlparse
+
+class Message:
+
+ def __init__(self, config, toolName, messageReference, messageIsRequest, remoteHost,
+ remotePort, serviceIsHttps, httpMethod, url, resourceType, statusCode,
+ responseContentType, message, interceptAction):
+ #create new Message object, based on data passed in from Burp
+ self.logger = logging.getLogger()
+ self.global_config = config
+ self.message = {}
+ self.message['tool'] = toolName
+ self.logger.debug("new Message object w/ reference : %s" % messageReference)
+ if (messageReference != '~'):
+ self.message['ref'] = messageReference + 1
+ else:
+ self.message['ref'] = messageReference
+ self.message['method'] = httpMethod
+ #parse out the url
+ self.j_url = url
+ if (str(url).startswith('http://') or str(url).startswith('https://')):
+ self.message['url'] = str(url)
+ else:
+ self.message['url'] = "%s://%s%s" % (('https' if serviceIsHttps else 'http'), remoteHost, url)
+ self.message['parsed-url'] = urlparse.urlparse(self.message['url'])
+ self.message['isreq'] = messageIsRequest
+ if messageIsRequest:
+ self.message['type'] = 'request'
+ else:
+ self.message['type'] = 'response'
+ self.message['ishttps'] = serviceIsHttps
+ self.message['raw'] = message
+ (self.message['headers'], self.message['body']) = self._separate_message(message, 'string')
+ (self.message['raw-headers'], self.message['raw-body']) = self._separate_message(message, 'raw')
+ self.message['parsed-headers'] = self._parse_headers()
+ self.message['referer'] = self.get_header('referer')
+ #Burp doesn't always provide the Content-type even if the header exists
+ if messageIsRequest == False:
+ if responseContentType != '':
+ self.message['contenttype'] = responseContentType
+ else:
+ self.message['contenttype'] = self._parse_content_type()
+ else:
+ self.message['contenttype'] = None
+ self.message['resourcetype'] = resourceType
+ self.message['statuscode'] = statusCode
+ self.message['remotehost'] = remoteHost
+ self.message['remoteport'] = remotePort
+ self.message['interceptaction'] = interceptAction[0]
+ self.message['highlight'] = None
+ self.message['comment'] = None
+
+ def is_changed(self):
+ if (self.message['raw'].tostring() == self.message['headers'] + self.message['body']):
+ return False
+ else:
+ return True
+
+ def from_proxy(self):
+ if (self.message['tool'] == 'proxy' and self.message['ref'] != '~'):
+ self.logger.debug("message is from proxy : %s" % self)
+ return True
+ self.logger.debug("message is NOT from proxy : %s" % self)
+ return False
+
+ def is_request(self):
+ return self.message['isreq']
+
+ def is_https(self):
+ return self.message['ishttps']
+
+ def set_intercept_action(self, intercept):
+ self.logger.debug("setting intercept action: %s" % intercept)
+ if intercept in self.global_config['internals']['intercept_actions']:
+ self.message['interceptaction'] = self.global_config['internals']['intercept_actions'][intercept]
+ else:
+ self.logger.error("could not set intercept action to '%s', using default '%s'" % (intercept, self.global_config['default_intercept_action']))
+ self.message['interceptaction'] = self.global_config['default_intercept_action']
+
+ def in_burp_scope(self):
+ jurl = URL(self['url'])
+ self.logger.debug("testing message scope for url: %s" % jurl)
+ return self.global_config['callbacks'].isInScope(jurl)
+
+ def is_highlighted(self):
+ if self.message['highlight'] != None:
+ return True
+ return False
+
+ def set_highlight(self, color):
+ if color in [ None, 'red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'pink', 'magenta', 'gray']:
+ self.message['highlight'] = color
+ else:
+ self.logger.error("highlight color '%s' is not valid, setting to None" % color)
+ self.message['highlight'] = None
+
+ def get_highlight(self):
+ return self.message['highlight']
+
+ def is_commented(self):
+ if 'comment' in self.message and self.message['comment'] != None:
+ self.logger.debug("is_commented returning True")
+ return True
+ return False
+
+ def set_comment(self, comment):
+ self.message['comment'] = comment
+
+ def get_comment(self):
+ return self.message['comment']
+
+ def host_in_domain(self, domain):
+ if self.message['remotehost'].endswith(domain):
+ return True
+ return False
+
+ def path_contains(self, pathfrag):
+ if pathfrag in self.message['parsed-url'].path:
+ return True
+ return False
+
+ def path_starts_with(self, pathfrag):
+ if self.message['parsed-url'].path.startswith(pathfrag):
+ return True
+ return False
+
+ def url_contains(self, expr):
+ if (re.search(expr, self.message['url'], re.IGNORECASE) != None):
+ return True
+ return False
+
+ def url_matches(self, expr):
+ if (re.match(expr, self.message['url'], re.IGNORECASE) != None):
+ return True
+ return False
+
+ def message_contains(self, expr):
+ if re.search(expr, self.message['headers'], re.IGNORECASE) or re.search(expr, self.message['body'], re.IGNORECASE):
+ return True
+ return False
+
+ def has_header(self, header):
+ return h.lower() in self.message['parsed-headers']
+
+ def has_headers(self, headers):
+ for h in headers:
+ if h.lower() not in self.message['parsed-headers']:
+ return False
+ return True
+
+ def headers_contain(self, expr):
+ self.logger.debug("headers_contain searching for expr '%s'" % expr)
+ for header in self.message['parsed-headers']:
+ if re.search(expr, self.message['parsed-headers'][header], re.IGNORECASE):
+ return True
+ return False
+
+ def header_contains(self, header, expr):
+ if header.lower() in self.message['parsed-headers']:
+ if re.search(expr, self.message['parsed-headers'][header], re.IGNORECASE):
+ return True
+ return False
+
+ def body_contains(self, expr):
+ if re.search(expr, self.message['body'], re.IGNORECASE):
+ return True
+ return False
+
+ def has_header(self, header):
+ if header.lower() in self.message['parsed-headers']:
+ return True
+ return False
+
+ def get_header(self, header):
+ if header.lower() in self.message['parsed-headers']:
+ return self.message['parsed-headers'][header.lower()]
+ return None
+
+ def update_content_length(self):
+ re_contentlength = re.compile('Content\-Length\:\s+\d+')
+ self.message['headers'] = re.sub(re_contentlength, "Content-Length: " +
+ str(len(self.message['body'])), self.message['headers'])
+
+ #helpers
+ def _parse_headers(self):
+ res = {}
+ for header in self.message['headers'].splitlines()[1:]:
+ header = header.split(":", 1)
+ if len(header) == 2:
+ res[header[0].strip().lower()] = header[1].strip()
+ return res
+
+ def _parse_content_type(self):
+ for header in self.message['headers'].splitlines()[1:]:
+ header = header.split(":", 1)
+ if len(header) == 2 and header[0].strip().lower() == 'content-type':
+ return header[1].split(";", 1)[0].strip()
+
+ def _separate_message(self, rawmsg, format):
+ headers = ''
+ content = ''
+ #find 13, 10, 13, 10 sequence (CRLF-CRLF marker between header and content)
+ for index,value in enumerate(rawmsg):
+ if value == 13:
+ try:
+ if (rawmsg[index+1] == 10 and rawmsg[index+2] == 13 and rawmsg[index+3] == 10):
+ headers = rawmsg[0:index+4]
+ content = rawmsg[index+4:]
+ break
+ except IndexError, e:
+ break
+ if (headers == ''):
+ #we didn't find the CRLF-CRLF marker -> case where there are only headers, no content
+ headers = message
+ content = array.array('B', '')
+ if (format == 'string'):
+ return (headers.tostring(), content.tostring())
+ else:
+ return (headers,content)
+
+ def __str__(self):
+ return "%s [%s] %s" % (self.message['type'], self.message['ref'], self.message['url'])
+
+ def __getitem__(self, key):
+ if key in self.message:
+ return self.message[key]
+ else:
+ return None
+
+ def __setitem__(self, key, value):
+ self.message[key] = value
View
183 hiccup/PluginManager.py
@@ -0,0 +1,183 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+import FileWatcher, GlobalConfig
+import SharedFunctions as shared
+
+import sys, os, re, logging, traceback, inspect
+
+class PluginManager:
+
+ file_watcher = None
+ dname = None
+ logger = None
+ auto_delete_class_files = False
+
+ def __init__(self, config):
+ self.global_config = config
+ self.logger = logging.getLogger()
+ self.logger.debug("initializing")
+ self.pluginmods = {}
+ self.pluginobjs = {}
+ if 'plugin_directory' not in config['defaults']:
+ raise Exception("'plugin_directory' not defined in config file" % (self.__module__))
+ else:
+ self.dname = config['defaults']['plugin_directory']
+ sys.path.append(self.dname)
+ if 'auto_delete_class_files' in config['defaults']:
+ if config['defaults']['auto_delete_class_files'] == True:
+ self.auto_delete_class_files = True
+ else:
+ self.auto_delete_class_files = False
+ #initialize plugins
+ plugins = self.find_plugins(self.dname)
+ for (i, pname) in enumerate(plugins):
+ self.enable_plugin(pname)
+ self.file_watcher = FileWatcher.FileWatcher(self.dname, ["%s.py" % pname for pname in plugins])
+
+ def find_plugins(self, dname):
+ plugins = []
+ dirList = os.listdir(dname)
+ for n in dirList:
+ if (os.path.isdir(n) == False):
+ m = re.match('(\S+)\.py$', n)
+ if (m):
+ plugins.append(m.group(1))
+ return plugins
+
+ def enable_plugin(self, pname):
+ try:
+ self.logger.debug("enabling plugin '%s'" % (pname))
+ try:
+ self.pluginmods[pname] = __import__(pname)
+ except Exception, e:
+ self.logger.error("exception importing '%s' plugin:\n%s" % (pname, shared.indent(traceback.format_exc().strip(), 1)))
+ else:
+ try:
+ self.pluginobjs[pname] = getattr(self.pluginmods[pname], pname)(self.global_config)
+ except AttributeError, e:
+ self.logger.error("plugin '%s' could not be loaded (%s)" % (pname, e))
+ self.disable_plugin(pname)
+ else:
+ if not self.pluginobjs[pname].required_config_loaded():
+ self.logger.error("plugin '%s' requires config section '%s' with parameters %s" % (pname, pname, self.pluginobjs[pname].required_config))
+ self.disable_plugin(pname)
+ else:
+ self.logger.info("enabled plugin '%s'" % (pname))
+ except Exception, e:
+ self.logger.error("exception when initializing plugin '%s':\n%s" % (pname, shared.indent(traceback.format_exc().strip(), 1)))
+ self.disable_plugin(pname)
+
+ def disable_plugin(self, pname):
+ self.logger.info("disabling plugin '%s'" % (pname))
+ if (pname in sys.modules):
+ del(sys.modules[pname])
+ if (pname in self.pluginmods):
+ del(self.pluginmods[pname])
+ if (pname in self.pluginobjs):
+ del(self.pluginobjs[pname])
+ if self.file_watcher != None:
+ self.file_watcher.remove_item("%s.py" % pname)
+ if self.auto_delete_class_files == True:
+ self.logger.debug(" testing for class file : %s" % (os.path.join("%s" % self.dname, "%s$py.class" % pname)))
+ if (os.path.isfile(os.path.join("%s" % self.dname, "%s$py.class" % pname))):
+ self.logger.debug(" disable_plugin removing stale .class file for disabled plugin '%s'" % pname)
+ try:
+ os.remove(os.path.join("%s" % self.dname, "%s$py.class" % pname))
+ except OSError, e:
+ self.logger.debug("failed to remove stale file %s$py.class but don't really care" % pname)
+
+ def reload_plugin(self, pname):
+ if pname in self.pluginmods and pname in self.pluginobjs:
+ try :
+ self.logger.debug("reloading plugin '%s'" % (pname))
+ self.pluginmods[pname] = reload(sys.modules[pname])
+ self.pluginobjs[pname] = getattr(self.pluginmods[pname], pname)(self.global_config)
+ except Exception, e:
+ self.logger.error("exception reloading '%s' plugin:\n%s" % (pname, shared.indent(traceback.format_exc().strip(), 1)))
+ self.disable_plugin(pname)
+ else:
+ if not self.pluginobjs[pname].required_config_loaded():
+ self.logger.error("plugin '%s' requires config section [%s] with parameters %s" % (pname, pname, self.pluginobjs[pname].required_config))
+ self.disable_plugin(pname)
+ else:
+ self.logger.info("reloaded plugin '%s'" % (pname))
+
+ def reload_changed(self):
+ plugins = self.find_plugins(self.dname)
+ for (i, pname) in enumerate(plugins):
+ if (pname not in self.pluginmods.keys()):
+ self.enable_plugin(pname)
+ self.file_watcher.add_item("%s.py" % pname)
+ for pname in self.pluginobjs.keys():
+ if (pname not in plugins):
+ self.disable_plugin(pname)
+ for pname in self.pluginmods.keys():
+ if (pname not in plugins):
+ self.file_watcher.remove_item("%s.py" % pname)
+ for fname in self.file_watcher.get_changed():
+ pname = ''.join(fname.split('.')[:-1])
+ self.reload_plugin(pname)
+
+ def reload_all(self):
+ plugins = self.find_plugins(self.dname)
+ self.logger.debug("reloading all plugins")
+ for (i, pname) in enumerate(plugins):
+ self.enable_plugin(pname)
+ self.file_watcher.add_item("%s.py" % pname)
+
+ def in_plugin_scope(self, message, key):
+ if self.pluginobjs[key].scope_all():
+ return True
+ elif self.pluginobjs[key].scope_proxy_only() and message.from_proxy():
+ return True
+ elif self.pluginobjs[key].scope_http_only() and message.from_proxy() == False:
+ return True
+ return False
+
+ def process_request(self, message):
+ self.logger.debug("process_request called")
+ for key in sorted(self.pluginobjs.keys()):
+ if self.in_plugin_scope(message, key):
+ self.logger.debug("plugin '%s' is in scope, processing" % key)
+ if (hasattr(self.pluginobjs[key], 'process_request') and inspect.ismethod(self.pluginobjs[key].process_request)):
+ try:
+ self.pluginobjs[key].process_request(message)
+ except Exception, e:
+ self.logger.error("exception in '%s' process_request():\n%s" % (key, shared.indent(traceback.format_exc().strip(), 1)))
+ else:
+ self.logger.error("skipping plugin '%s', process_request not defined" % key)
+ else:
+ self.logger.debug("plugin '%s' is not in scope, SKIPPING" % key)
+
+ def process_response(self, message):
+ self.logger.debug("process_response called")
+ for key in sorted(self.pluginobjs.keys()):
+ if self.in_plugin_scope(message, key):
+ self.logger.debug("plugin '%s' is in scope, processing" % key)
+ if (hasattr(self.pluginobjs[key], 'process_response') and inspect.ismethod(self.pluginobjs[key].process_response)):
+ try:
+ self.pluginobjs[key].process_response(message)
+ except Exception, e:
+ self.logger.error("exception in '%s' process_response():\n%s" % (key, shared.indent(traceback.format_exc().strip(), 1)))
+ else:
+ self.logger.error("skipping plugin '%s', process_response not defined" % key)
+ else:
+ self.logger.debug("plugin '%s' is not in scope, SKIPPING" % key)
+
+ def process_menuitem_click(self, caption, message):
+ self.logger.debug("process_menuitem_click() called with caption '%s'" % caption)
+ self.logger.debug("current handler_map : %s" % self.global_config['internals']['handler_map'])
+ if (caption in self.global_config['internals']['handler_map']):
+ pname = self.global_config['internals']['handler_map'][caption]
+ self.logger.debug("click '%s' maps to plugin '%s'" % (caption, pname))
+ if (pname in self.pluginobjs):
+ if (hasattr(self.pluginobjs[pname], 'process_menuitem_click') and inspect.ismethod(self.pluginobjs[pname].process_menuitem_click)):
+ self.pluginobjs[pname].process_menuitem_click(caption, message)
+ else:
+ self.logger.error("could not process menu click, plugin '%s' has no process_menuitem_click() function" % pname)
+ else:
+ self.logger.error("could not process menu click, plugin '%s' not loaded" % pname)
+ else:
+ self.logger.error("could not process menu click, no mapping to plugin")
+
View
65 hiccup/SharedFunctions.py
@@ -0,0 +1,65 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+import re
+import json
+import array
+
+from openpyxl import Workbook
+from openpyxl.style import Alignment
+from openpyxl.cell import get_column_letter
+
+def update_json_values(jsonobj, updates, changed):
+ if (jsonobj != None):
+ if (isinstance(jsonobj,list)):
+ for i in jsonobj:
+ changed = update_json_values(i, updates, changed)
+ elif (isinstance(jsonobj,dict)):
+ for (k,v) in jsonobj.items():
+ if (k in updates.keys()):
+ jsonobj[k] == updates[k]
+ changed = True
+ changed = update_json_values(v, updates, changed)
+ return changed
+
+def indent(data, tabs):
+ newstr = ''
+ for line in data.splitlines():
+ newstr = newstr + '\t'*tabs + line + '\n'
+ return newstr.rstrip()
+
+def pprint_table(self, table):
+ """Prints out a table of data, padded for alignment
+ @param table: The table to print. A list of lists.
+ Each row must have the same number of columns. """
+ col_paddings = []
+
+ for i in range(len(table[0])):
+ col_paddings.append(max([len(str(row[i])) for row in table]))
+
+ for row in table:
+ # left col
+ print row[0].ljust(col_paddings[0] + 1),
+ # rest of the cols
+ for i in range(1, len(row)):
+ col = str(row[i]).rjust(col_paddings[i] + 2)
+ print col,
+ print
+
+def write_xlsx(filename, worksheet, columns, table):
+ wb = Workbook()
+ ws = wb.worksheets[0]
+ ws.title = worksheet
+ for (col_letter, col_title, col_width) in columns:
+ ws.cell("%s1" % (col_letter)).value = col_title
+ ws.cell("%s1" % (col_letter)).style.font.bold = True
+ ws.column_dimensions[col_letter].width = col_width
+ for r in ws.range("%s1:%s%d" % (col_letter, col_letter, len(table)+1)):
+ for c in r:
+ c.style.alignment.horizontal = Alignment.HORIZONTAL_LEFT
+ ws._set_auto_filter("A1:%s1" % (col_letter)) #uses last col_letter value from loop above
+ for row_idx in range(0, len(table)):
+ for col_idx in range(0, len(table[row_idx])):
+ ws.cell("%s%s" % (get_column_letter(col_idx+1), row_idx+2)).value = table[row_idx][col_idx]
+ wb.save('%s' % (filename))
+
View
4 hiccup/__init__.py
@@ -0,0 +1,4 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+__author__ = "Jamie Finnigan <jfinnigan@zynga.com>"
View
BIN lib/BurpExtender.jar
Binary file not shown.
View
BIN lib/sqlitejdbc-v056.jar
Binary file not shown.
View
90 plugins/README.md
@@ -0,0 +1,90 @@
+Hiccup Plugins
+==============
+
+Example Plugin
+--------------
+A basic debug plugin template looks like this:
+
+ from hiccup import BasePlugin
+ from hiccup import SharedFunctions as shared
+
+ class Debug (BasePlugin.BasePlugin):
+
+ required_config = []
+ plugin_scope = 'proxy_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ self.logger.info("processing '%s' request %s" % (message['type'], message))
+
+ def process_response(self, message):
+ self.logger.info("processing '%s' response %s" % (message['type'], message))
+
+
+Plugin Manager
+--------------
+
+The PluginManager class (hiccup/PluginManager.py) locates, loads, reloads, and unloads plugins according to file existence/changes as monitored by FileWatcher. It also pushes messages from Burp Extender through to individual plugins.
+
+Hiccup uses the plugin\_directory configuration flag to identify the location where plugins are to be loaded from. Plugins that are not currently required to be loaded can can be stored in subdirectories inside of this location.
+
+The plugin\_directory location will be automatically monitored by the PluginManager, which will load, unload, and reload plugins automatically (as they are moved into or out of the directory, or as changed are detected to individual plugin files).
+
+As plugins are loaded, Jython will automatically create a $py.class file. These can be automatically removed whena plugin is unloaded (if the global configuration flag 'auto\_delete\_class\_files' is set to True), or can be manually deleted if necessary.
+
+
+Plugin Structure
+----------------
+A plugin should:
+
+* Have a unique filename (e.g. UniqueName.py) and define a class with that same name (e.g. UniqueName) that extends BasePlugin.BasePlugin.
+
+* Define an \_\_init\_\_() function, that at a minimum calls BasePlugin.BasePlugin.\_\_init\_\_(self, global\_config). This constructor may also be used to register menu items, setup other variables used by the plugin, etc.
+
+* Define, as required, process\_request() and process\_response() functions that accept an argument 'message' (a Message object).
+
+* Defined, as required, a process\_menuitem\_click() function that accepts a 'caption' (string with button label) and 'message' (array of Message objects) arguments.
+
+* Define required\_config, a list of variables that this plugin requires be defined in the Hiccup configuration file. If no configuration items are required then this does not need to be defined.
+
+* Define plugin\_scope, a variable set to one of 'all', 'proxy\_only', or 'http\_only'. (This is used by Hiccup to determine what message types this particular plugin should be executed for. If the scope is not defined in the plugin, it will revert to a global default per the configuration file.)
+
+A plugin can access its required\_config items through the global\_config object. It can also access the IBurpExtenderCallbacks interface through global\_config['callbacks'], and can store temporary data in global\_config['state'].
+
+There is currently minimal plugin validation performed by Hiccup. Erroneously-defined plugins may cause undetected errors when loaded by PluginManager, but error details should be included in other console output. Third-party plugins should be reviewed for correctness and security prior to use.
+
+Plugin Scope
+------------
+The PluginManager executes plugins for each message based on the scope defined for that plugin. Scope maps to the processHttpMessage() and processProxyMessage() functions defined by Burp Extender. Plugin scope is treated as follows:
+
+* A plugin with scope 'proxy\_only' will be executed only for messages associated with the Burp Proxy component. (Plugins are executed only for processProxyMessage() calls.)
+
+* A plugin with scope 'http\_only' scope will be executed for messages associated with all Burp components (Proxy, Repeater, Intruder, Scanner, etc). Messages will only ever be processed once. (Plugins are executed only for processHttpMessage() calls.)
+
+* A plugin with scope 'all' will be executed for every message. Messages that pass through the Burp Proxy component will be processed twice. (Plugins are executed for both processHttpMessage() and processProxyMessage() calls.)
+
+Scope is not relevant when plugins are executed by a menu click event.
+
+Plugin Distribution
+-------------------
+Should you wish to distribute your plugins to others, you can share the file directly (say, as a mailing list / forum post or gist, or by providing the .py file as a download). Alternatively, you could setup a Github repository and allow people to use git to clone your repository and pull updates as necessary.
+
+Again, third-party plugins should not necessarily be trusted and should be reviewed for correctness and security prior to use.
+
+
+Authors
+=======
+Hiccup was developed by Jamie Finnigan (http://twitter.com/chair6), based on initial work by David Robert (http://blog.ombrepixel.com/post/2010/08/30/Extending-Burp-Suite-in-Python), and is continued as an official Zynga OpenSource (http://code.zynga.com/) project.
+
+
+License
+=======
+Copyright (c) 2012 Zynga Inc. http://zynga.com/
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
33 plugins/disabled/AmfPrint.py
@@ -0,0 +1,33 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+import pyamf.remoting, pprint
+
+# plugin to pretty-print AMF packets as they pass through the proxy
+
+class AmfPrint (BasePlugin.BasePlugin):
+
+ plugin_scope = 'proxy_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ def process_message(self, message):
+ if message.header_contains('content-type', 'application\/x-amf'):
+ self.logger.info("AMF %s" % (message))
+ try:
+ amfdata = pyamf.remoting.decode(message['body'])
+ except pyamf.DecodeError:
+ self.logger.error("Content-type is set to application/x-amf, but input does not appear to be valid AMF.")
+ else:
+ for (key,msg) in amfdata.bodies:
+ self.logger.info(pprint.pformat(msg.body, indent=2))
View
32 plugins/disabled/Commenter.py
@@ -0,0 +1,32 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+# add comments to messages that will be visible in Burp 'comment' field, based on certain test results
+
+class Commenter (BasePlugin.BasePlugin):
+
+ plugin_scope = 'http_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ def process_message(self, message):
+ self.logger.debug("processing %s" % (message))
+ if message.host_in_domain('google.com'):
+ message.set_comment('In the google.com domain')
+ if message.body_contains('internal only'):
+ message.set_comment('Possibly sensitive information')
+ if message.url_contains('admin'):
+ message.set_comment('Possible admin element')
+
+
+
View
47 plugins/disabled/ConditionalIntercepts.py
@@ -0,0 +1,47 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+# intercept messages in Burp, based on certain test results
+
+class ConditionalIntercepts (BasePlugin.BasePlugin):
+
+ required_config = ['host_contains', 'url_contains', 'body_contains', 'header_exists', 'headers_contain']
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ def process_message(self, message):
+ self.logger.debug("process_message called w/ config : %s" % self.global_config[self.plugin_name])
+ for searchstr in self.global_config[self.plugin_name]['host_contains']:
+ if (searchstr in message['remotehost']):
+ self.do_intercept(message, searchstr)
+ return
+ for searchstr in self.global_config[self.plugin_name]['url_contains']:
+ if (searchstr in message['url']):
+ self.do_intercept(message, searchstr)
+ return
+ for searchstr in self.global_config[self.plugin_name]['body_contains']:
+ if (searchstr in message['body']):
+ self.do_intercept(message, searchstr)
+ return
+ for searchstr in self.global_config[self.plugin_name]['header_exists']:
+ if message.has_header(searchstr):
+ self.do_intercept(message, searchstr)
+ return
+ for searchstr in self.global_config[self.plugin_name]['headers_contain']:
+ if message.headers_contain(searchstr):
+ self.do_intercept(message, searchstr)
+ return
+
+ def do_intercept(self, message, searchstr):
+ message.set_intercept_action("DO_INTERCEPT")
+ self.logger.info("found match for \'%s\' in %s" % (searchstr, message))
View
32 plugins/disabled/ContentReplace.py
@@ -0,0 +1,32 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+import re
+
+# simple content replacement plugin
+
+class ContentReplace (BasePlugin.BasePlugin):
+
+ required_config = ['targets',]
+ plugin_scope = 'proxy_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ def process_message(self, message):
+ for target in self.global_config[self.plugin_name]['targets']:
+ strfind = target[0]
+ strreplace = target[1]
+ (message['body'], count) = re.subn(strfind, strreplace, message['body'])
+ if count > 0:
+ self.logger.info("replaced %s instances of '%s' with '%s' in %s" % (count, strfind, strreplace, message))
+
View
20 plugins/disabled/ContentType.py
@@ -0,0 +1,20 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+# prints Content-type from header for each message that passes through the plugin
+
+class ContentType (BasePlugin.BasePlugin):
+
+ plugin_scope = 'http_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ pass
+
+ def process_response(self, message):
+ self.logger.info("Content-type: %s in %s" % (message['contenttype'], message))
View
59 plugins/disabled/CookieProfiler.py
@@ -0,0 +1,59 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+from synchronize import *
+
+import re
+
+# generate an Excel spreadsheet containing list of domains and associated cookies
+
+class CookieProfiler (BasePlugin.BasePlugin):
+
+ required_config = ['output_file', 'write_after']
+ plugin_scope = 'http_only'
+
+ str_cookies = '^((Cookie)|(Set-Cookie)): (?P<cookie>.*)$'
+ re_cookies = re.compile(str_cookies)
+
+ results_columns = [('A', 'Host', 50), ('B', 'Key', 30), ('C', 'Values', 50)]
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+ self.output_file = global_config[self.plugin_name]['output_file']
+ self.write_after = global_config[self.plugin_name]['write_after']
+ self.cookiejar = {}
+ self.count = 0
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ @make_synchronized #decorator to lock access to this function for a single thread at a time
+ def update_results(self, codata):
+ data = []
+ for host in sorted(codata):
+ for key in sorted(codata[host]):
+ data.append([host, key, ','.join(sorted(codata[host][key]))])
+ shared.write_xlsx(self.output_file, 'CookieSummary', self.results_columns, data)
+ self.logger.info("count=%d, writing results to file: %s" % (self.count, self.output_file))
+
+ def process_message(self, message):
+ self.count = self.count + 1
+ if (self.count % self.write_after == 0):
+ self.update_results(self.cookiejar)
+
+ for line in message['headers'].splitlines():
+ res = self.re_cookies.match(line)
+ if (res):
+ if (message['remotehost'] not in self.cookiejar):
+ self.cookiejar[message['remotehost']] = {}
+ for (key,val) in [(ckey[0].strip(),ckey[-1].strip()) for ckey in [keyval.split('=') for keyval in res.group('cookie').split(';')]]:
+ if (key not in self.cookiejar[message['remotehost']]):
+ self.cookiejar[message['remotehost']][key] = set()
+ if (val != key):
+ self.cookiejar[message['remotehost']][key].add(val)
View
82 plugins/disabled/DatabaseLogger.py
@@ -0,0 +1,82 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+from java.sql import DriverManager
+from java.sql import SQLException
+from java.lang import Class
+
+import os, sys
+
+# plugin to log all requests/responses, and content of certain types, to an SQLite database
+# - filename that data will be logged to is set here:
+
+class DatabaseLogger (BasePlugin.BasePlugin):
+
+ required_config = ['storable_types', 'output_file']
+ plugin_scope = 'proxy_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+ self.dbfile = self.global_config[self.plugin_name]['output_file']
+ self.dburl = "jdbc:sqlite:" + self.dbfile
+ dbdriver = "org.sqlite.JDBC"
+ Class.forName(dbdriver)
+ if (os.path.isfile(self.dbfile) == True):
+ #use existing db schema
+ self.logger.info("%s already exists, will be appending to database" % (self.dbfile))
+ self.db = DriverManager.getConnection(self.dburl)
+ stmt = self.db.createStatement()
+ else:
+ #create db file
+ self.logger.info("creating db file %s" % (self.dbfile))
+ self.db = DriverManager.getConnection(self.dburl)
+ stmt = self.db.createStatement()
+ stmt.executeUpdate('''CREATE TABLE IF NOT EXISTS "hiccuplog" (ref INTEGER, type BOOLEAN, url TEXT, headers TEXT, content BLOB)''')
+
+ def __del__(self):
+ self.db.close()
+ BasePlugin.BasePlugin.__del__(self)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ def process_message(self, message):
+ self.logger.info("logging %s" % (message))
+ try:
+ prep = self.db.prepareStatement("INSERT INTO hiccuplog VALUES (?,?,?,?,?)")
+ if (message['ref'] != '~'):
+ prep.setInt(1, message['ref'])
+ else:
+ prep.setInt(1, -1)
+ if message.is_request():
+ prep.setBoolean(2, 0)
+ else:
+ prep.setBoolean(2, 1)
+ prep.setString(3, message['url'])
+ prep.setString(4, message['headers'])
+ # limit the MIME types for which we will save content to the database
+ if (self.is_storable_content(message['contenttype'])):
+ prep.setString(5, message['body'])
+ else:
+ prep.setString(5, 'CONTENT NOT STORED (Content-type: %s)' % message['contenttype'])
+ prep.addBatch()
+ prep.executeBatch()
+ except IOError, msg:
+ self.logger.error("problem preparing query - %s" % (msg))
+ except SQLException, msg:
+ self.logger.error("database problem - %s" % (msg))
+
+ def is_storable_content(self, ctype):
+ if ctype == None:
+ return True
+ else:
+ for stortype in self.global_config[self.plugin_name]['storable_types']:
+ if ctype.startswith(stortype):
+ return True
+ return False
View
26 plugins/disabled/Debug.py
@@ -0,0 +1,26 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+# simple debug plugin
+
+class Debug (BasePlugin.BasePlugin):
+
+ plugin_scope = 'proxy_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+ self.global_config.register_menuitem("%s test" % self.plugin_name, self.plugin_name)
+
+ def process_request(self, message):
+ self.logger.info("processing '%s' %s" % (message['tool'], message))
+
+ def process_response(self, message):
+ self.logger.info("processing '%s' %s" % (message['tool'], message))
+
+ def process_menuitem_click(self, caption, messages):
+ self.logger.info("'%s' menu item clicked" % caption)
+ for m in messages:
+ self.logger.info(" processing selected message : %s" % m)
View
37 plugins/disabled/DropMatches.py
@@ -0,0 +1,37 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+# automatically drop messages that meet certain criteria
+
+class DropMatches (BasePlugin.BasePlugin):
+
+ required_config = ['hosts', 'domains']
+ plugin_scope = 'proxy_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ def process_message(self, message):
+ if message['remotehost'] in self.global_config[self.plugin_name]['hosts']:
+ self.drop(message)
+ return
+ else:
+ for d in self.global_config[self.plugin_name]['domains']:
+ if message.host_in_domain(d):
+ self.drop(message)
+ break
+
+ def drop(self, message):
+ message.set_intercept_action('DROP')
+ self.logger.info("dropping message for host %s" % message['remotehost'])
+
+
View
31 plugins/disabled/HeaderReplace.py
@@ -0,0 +1,31 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+import re
+
+# perform substitions against headers
+
+class HeaderReplace (BasePlugin.BasePlugin):
+
+ required_config = ['targets',]
+ plugin_scope = 'proxy_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ def process_message(self, message):
+ for target in self.global_config[self.plugin_name]['targets']:
+ strfind = target[0]
+ strreplace = target[1]
+ (message['headers'], count) = re.subn(strfind, strreplace, message['headers'])
+ if count > 0:
+ self.logger.info("replaced %s matching '%s' header/s %s" % (count, strfind, message))
View
29 plugins/disabled/Highlighter.py
@@ -0,0 +1,29 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+# highlight messages that will be visible in Burp, based on certain test results
+
+class Highlighter (BasePlugin.BasePlugin):
+
+ plugin_scope = 'http_only'
+
+ #accepted colors : 'red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'pink', 'magenta', 'gray'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ def process_message(self, message):
+ self.logger.debug("processing %s" % (message))
+ if message.host_in_domain('google.com'):
+ message.set_highlight('cyan')
+ if message.body_contains('internal only'):
+ message.set_highlight('red')
View
36 plugins/disabled/InteractiveIntercept.py
@@ -0,0 +1,36 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+from code import InteractiveConsole
+
+from synchronize import *
+
+# perform interactive intercept of requests; could base it on certain criteria
+# - haven't found this particularly useful but could be extended or fit other's flow better
+
+class InteractiveIntercept (BasePlugin.BasePlugin):
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ @make_synchronized
+ def process_message(self, message):
+ self.logger.info("processing %s" % (message))
+ self.message = message
+ from pprint import pprint
+ loc=dict(locals())
+ loc['message'] = self.message
+ c = InteractiveConsole(locals=loc)
+ c.interact("interactive intercept")
+ for key in loc:
+ if key != '__builtins__':
+ exec "%s = loc[%r]" % (key, key)
View
70 plugins/disabled/JsonManipulation.py
@@ -0,0 +1,70 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+import json
+import re
+
+# plugin to automatically manipulate JSON data structures as they pass through the proxy
+
+class JsonManipulation (BasePlugin.BasePlugin):
+
+ required_config = ['delete_keys', 'set_values']
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+
+ def process_message(self, message):
+ m = re.search('content-type\:\s+application\/json(; charset=(\S+)){0,1}', message['headers'], flags=re.IGNORECASE)
+ if (m):
+ try:
+ jsondata = json.read(message['body'])
+ except Exception, e:
+ self.logger.error("exception reading JSON: %s" % e)
+ else:
+ count = self.traverse_json_obj(jsondata, 0, 0)
+ self.logger.info("made %s modifications in %s" % (count, message))
+ if (count > 0):
+ message['body'] = json.write(jsondata)
+
+ def traverse_json_obj(self, obj, level, count):
+ level = level + 1
+ if (obj == None):
+ pass
+ elif (isinstance(obj,str) or isinstance(obj,unicode)):
+ pass
+ elif (isinstance(obj,int) or isinstance(obj,long) or isinstance(obj,float)):
+ pass
+ elif (isinstance(obj,bool)):
+ pass
+ elif(isinstance(obj,dict)):
+ #all manipulation is at the dict level
+ for delkey in self.global_config[self.plugin_name]['delete_keys']:
+ if (delkey in obj.keys()):
+ del obj[delkey]
+ count = count + 1
+ for (setkey, setval) in self.global_config[self.plugin_name]['set_values'].iteritems():
+ if (setkey in obj.keys()):
+ #deal with list case
+ if (isinstance(obj[setkey], list)):
+ for i in range(0,len(obj[setkey])):
+ obj[setkey][i] = setval
+ #otherwise it's just a single value to replace
+ else:
+ obj[setkey] = setval
+ count = count + 1
+ for (k,subobj) in obj.iteritems():
+ count = self.traverse_json_obj(subobj, level, count)
+ else:
+ for (subobj) in obj:
+ count = self.traverse_json_obj(subobj, level, count)
+ return count
View
34 plugins/disabled/JsonPrint.py
@@ -0,0 +1,34 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+import re, pprint, json
+
+# plugin to pretty-print JSON packets as they pass through the proxy
+
+class JsonPrint (BasePlugin.BasePlugin):
+
+ plugin_scope = 'proxy_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+
+ def process_message(self, message):
+ m = re.search('content-type\:\s+application\/json(; charset=(\S+)){0,1}', message['headers'], flags=re.IGNORECASE)
+ if (m):
+ try:
+ jsondata = json.read(message['body'])
+ except Exception, e:
+ self.logger.error("exception while reading JSON: %s" % e)
+ else:
+ self.logger.info("JSON detected in %s" % message)
+ self.logger.info("\t%s" % pprint.pformat(jsondata))
View
28 plugins/disabled/PlaintextHighlighter.py
@@ -0,0 +1,28 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+# apply highlight ta all messages that are HTTP, rather than HTTPS/SSL/TLS
+
+class PlaintextHighlighter (BasePlugin.BasePlugin):
+
+ required_config = []
+ plugin_scope = 'http_only'
+
+ #legit colors : 'red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'pink', 'magenta', 'gray'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ self.process_message(message)
+
+ def process_response(self, message):
+ self.process_message(message)
+
+ def process_message(self, message):
+ self.logger.debug("processing %s" % (message))
+ if message.is_https() == False:
+ message.set_highlight('red')
View
26 plugins/disabled/ScopeTest.py
@@ -0,0 +1,26 @@
+# Hiccup - Burp Suite Python Extensions
+# Copyright 2012 Zynga Inc.
+
+from hiccup import BasePlugin
+from hiccup import SharedFunctions as shared
+
+# simple debug plugin, that includes Burp scope in decision to process
+
+class ScopeTest (BasePlugin.BasePlugin):
+
+ plugin_scope = 'proxy_only'
+
+ def __init__(self, global_config):
+ BasePlugin.BasePlugin.__init__(self, global_config, self.required_config, self.plugin_scope)
+
+ def process_request(self, message):
+ if (message.in_burp_scope()):
+ self.logger.info("processing %s" % (message))
+ else:
+ self.logger.info("not in scope, skipping %s" % (message))
+
+ def process_response(self, message):
+ if (message.in_burp_scope()):
+ self.logger.info("processing %s" % (message))
+ else:
+ self.logger.info("not in scope, skipping %s" % (message))

0 comments on commit b0bb1ad

Please sign in to comment.
Something went wrong with that request. Please try again.