Version 0.2.11

master
Rob Glew 9 years ago
parent 9d274de709
commit 992edab315
  1. 153
      README.md
  2. 2
      docs/source/contributing.rst
  3. 15
      docs/source/pappyplugins.rst
  4. 2
      pappyproxy/__init__.py
  5. 24
      pappyproxy/config.py
  6. 53
      pappyproxy/http.py
  7. 102
      pappyproxy/macros.py
  8. 12
      pappyproxy/plugin.py
  9. 68
      pappyproxy/plugins/macrocmds.py
  10. 77
      pappyproxy/plugins/misc.py
  11. 7
      pappyproxy/plugins/view.py
  12. 19
      pappyproxy/session.py
  13. 7
      pappyproxy/templates/macro.py.template
  14. 27
      pappyproxy/templates/macro_header.py.template
  15. 34
      pappyproxy/templates/macro_resubmit.py.template
  16. 8
      pappyproxy/templates/macroheader.py.template
  17. 25
      pappyproxy/tests/test_session.py

@ -44,6 +44,8 @@ Table of Contents
* [Useful Functions](#useful-functions) * [Useful Functions](#useful-functions)
* [Intercepting Macros](#intercepting-macros) * [Intercepting Macros](#intercepting-macros)
* [Enabling/Disabling Intercepting Macros](#enablingdisabling-intercepting-macros) * [Enabling/Disabling Intercepting Macros](#enablingdisabling-intercepting-macros)
* [Macro Templates](#macro-templates)
* [Resubmitting Groups of Requests](#resubmitting-groups-of-requests)
* [Logging](#logging) * [Logging](#logging)
* [Additional Commands and Features](#additional-commands-and-features) * [Additional Commands and Features](#additional-commands-and-features)
* [Response streaming](#response-streaming) * [Response streaming](#response-streaming)
@ -54,6 +56,7 @@ Table of Contents
* [Using an HTTP Proxy](#using-an-http-proxy) * [Using an HTTP Proxy](#using-an-http-proxy)
* [Using a SOCKS Proxy](#using-a-socks-proxy) * [Using a SOCKS Proxy](#using-a-socks-proxy)
* [Transparent Host Redirection](#transparent-host-redirection) * [Transparent Host Redirection](#transparent-host-redirection)
* [Project File Encryption](#project-file-encryption)
* [FAQ](#faq) * [FAQ](#faq)
* [Why does my request have an id of --?!?!](#why-does-my-request-have-an-id-of---) * [Why does my request have an id of --?!?!](#why-does-my-request-have-an-id-of---)
* [Boring, Technical Stuff](#boring-technical-stuff) * [Boring, Technical Stuff](#boring-technical-stuff)
@ -698,6 +701,13 @@ def run_macro(args):
If you enter in a value for `SHORT_NAME`, you can use it as a shortcut to run that macro. So if in a macro you set `SHORT_NAME='tm'` you can run it by running `pappy> rma tm`. If you enter in a value for `SHORT_NAME`, you can use it as a shortcut to run that macro. So if in a macro you set `SHORT_NAME='tm'` you can run it by running `pappy> rma tm`.
Remember, you can use the wildcard to generate a macro with all in-context requests:
```
# Generate a macro with all in-context requests
pappy> gma allreqs *
```
### Passing Arguments to Macros ### Passing Arguments to Macros
When you run the macro, any additional command line arguments will be passed to the run_macro function in the `args` argument. For example, if you run your macro using When you run the macro, any additional command line arguments will be passed to the run_macro function in the `args` argument. For example, if you run your macro using
@ -802,6 +812,7 @@ def run_macro(args):
| get_request(url, url_params={}) | Returns a Request object that contains a GET request to the given url with the given url params | | get_request(url, url_params={}) | Returns a Request object that contains a GET request to the given url with the given url params |
| post_request(url, post_params={}, url_params={}) | Returns a Request object that contains a POST request to the given url with the given url and post params | | post_request(url, post_params={}, url_params={}) | Returns a Request object that contains a POST request to the given url with the given url and post params |
| request_by_id(reqid) | Get a request object from its id. | | request_by_id(reqid) | Get a request object from its id. |
| main_context_ids() | Returns a list of the IDs that are in the current context. Use this for macros that need to act on every in-context request. For example, it can be used in a macro to resubmit a set of requests. |
Intercepting Macros Intercepting Macros
------------------- -------------------
@ -903,6 +914,77 @@ You can use the following commands to start/stop intercepting macros
| `lim` | `list_int_macros`, `lsim` | List all enabled/disabled intercepting macros | | `lim` | `list_int_macros`, `lsim` | List all enabled/disabled intercepting macros |
| `gima <name>` | `generate_int_macro`, `gima` | Generate an intercepting macro with the given name. | | `gima <name>` | `generate_int_macro`, `gima` | Generate an intercepting macro with the given name. |
Macro Templates
---------------
Pappy also includes some other templates for generating macros. They can be generated with the `gtma` command. You can then modify the generated macros to do what you want. For example, you could modify the resubmit macro to get a new session token before submitting each request. Using a template can save you from writing boilerplate for commonly created macros.
Examples:
```
# The same as gma foo 1,2,3
pappy> gtma foo macro 1,2,3
Wrote script to macro_foo.py
# Generate a macro that resubmits all in-context requests
pappy> gtma suball resubmit
Wrote script to macro_suball.py
# Generate an intercepting macro that modifies headers as they pass through the proxy
pappy> gtma headers modheader
Wrote script to int_headers.py
```
Command information:
| Command | Aliases | Description |
|:--------|:--------|:------------|
| `gtma <name> <template name> [template arguments]` | `generate_template_macro`, `gtma` | Generate a macro using a template. |
Available macro templates:
| Name | Arguments | Description |
|:-----|:----------|:------------|
| `macro` | `[reqids]` | The template used to generate macros from request IDs. |
| `intmacro` | None | The template used to generate an intercepting macro. |
| `modheader` | None | Create an intercepting macro that modifies a header in the request or response. |
| `resubmit` | None | Create a macro that resubmits all in-context requests. Includes commented out code to maintain session state using a cookie jar. |
Resubmitting Groups of Requests
-------------------------------
You can use the `submit` request to resubmit requests. It is suggested that you use this command with a heavy use of filters and using the wildcard (`*`) to submit all in-context requests. Be careful submitting everything in context, remember, if you have to Ctl-C out you will close Pappy and lose all in-memory requests!
| Command | Aliases | Description |
|:--------|:--------|:------------|
| `submit reqids [-m] [-u] [-p] [-c [COOKIES [COOKIES ...]]] [-d [HEADERS [HEADERS ...]]]` | `submit` | Submit a given set of requests. Request IDs must be passed in as the first argument. The wildcard (`*`) selector can be very useful. Resubmitted requests are given a `resubmitted` tag. See the arguments section for information on the arguments. |
### Useful Filters For Selecting Requests to Resubmit
* `before` and `after` to select requests in a time range. You can use the `after` filter on the most recent request, browse the site, then use the `before` filter to select a continuous browsing session.
* `verb` if you only want to select GET requests
* `path ct logout` to avoid logging out
### Arguments
There are a few simple parameters you can pass to the command to modify requests. These behave like normal command parameters in the terminal. If you need something more complex (ie getting CSRF tokens, refreshing the session token, reacting to Set-Cookie headers, etc.) you should consider writing a macro and using the `main_context_ids` function to get in-context IDs then iterating over them and handling them however you want.
| Argument | Description |
|:---------|:------------|
| `-c <cookie>=<val>` | Modify a cookie on each request before submitting. Can pass more than one pair to the flag to modify more than one cookie. Does not encode the cookie values in any way. |
| `-d <header>=<val>` | Modify a header on each request before submitting. Can pass more than one pair to the flag to modify more than one header. |
| `-m` | Store requests in memory instead of saving to the data file. |
| `-u` | Only submit one request per endpoint. Will count requests with the same path but different url params as *different* endpoints. |
| `-p` | Only submit one request per endpoint. Will count requests with the same path but different url params as *the same* endpoints. |
Examples:
```
# Resubmit all in-context requests with the SESSIONID cookie set to 1234 and SESSIONSTATE set to {'admin'='true'}
pappy> submit * -c SESSIONID=1234 SESSIONSTATE=%7B%27admin%27%3A%27true%27%7D
# Resubmit all in-context requests with the User-Agent header set to "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" then store them in memory
pappy> submit * -m -h "User-Agent=Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
# Submit requests 123, 124, and 125 with a new user agent and new session cookies and store the submitted requests in memory
pappy> submit 123,124,125 -h "User-Agent=Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" -c SESSIONID=1234 SESSIONSTATE=%7B%27admin%27%3A%27true%27%7D
```
Logging Logging
------- -------
You can watch in real-time what requests are going through the proxy. Verbosisty defaults to 1 which just states when connections are made/lost and some information on what is happening. If verbosity is set to 3, it includes all the data which is sent through the proxy and processed. It will print the raw response from the server, what it decodes it to, etc. Even if you don't run this command, all the information is stored in the dubug directory (the directory is cleared every start though!) You can watch in real-time what requests are going through the proxy. Verbosisty defaults to 1 which just states when connections are made/lost and some information on what is happening. If verbosity is set to 3, it includes all the data which is sent through the proxy and processed. It will print the raw response from the server, what it decodes it to, etc. Even if you don't run this command, all the information is stored in the dubug directory (the directory is cleared every start though!)
@ -1110,6 +1192,72 @@ Or if you’re going to YOLO it do the same thing then listen on port 80/443 dir
Pappy will automatically use this host to make the connection and forward the request to the new server. Pappy will automatically use this host to make the connection and forward the request to the new server.
Project File Encryption
-----------------------
Pappy includes some basic features for automatically compressing and encrypting your project directory with a password. However, before I go into details on how to do this, I need to make one thing clear.
**Don't rely on Pappy to encrypt confidential information. Use a dedicated encryption product to encrypt your project directory instead.**
Other commercial and large open source crypto projects have had a much larger number of people look at their crypto implementations and are less likely to have errors in their implementation. However, for cases where you don't need enterprise level security or if you just want your project stored in a single password-protected file instead of a directory, Pappy's got you covered.
Here is how Pappy's project encryption works:
* Open a project by running Pappy with the `-c` flag
* Pappy creates a `crypt/` directory in the current directory and changes the working directory into it
* Do work as normal. You can use other tools in the created `crypt/` directory
* When you quit Pappy, the file is compressed and encrypted with the provided password
* The project directory is deleted
Unfortunately, if Pappy hard crashes the files will not be cleaned up. However, if you start Pappy and it notices a `crypt/` directory, it will attempt to use it as the project directory and create a new encrypted project file upon exiting.
Here is an of the usage:
```
$ pwd
/tmp/exampleproj
$ ls
$ pappy -c example.proj
Copying default config to ./config.json
Proxy is listening on port 8000
pappy> !pwd
/tmp/exampleproj/crypt
# Switch to another terminal window
/templates/ $ echo "Hello World" > /tmp/exampleproj/crypt/hello.txt
# Back to Pappy
pappy> !cat hello.txt
Hello World
pappy> exit
Enter a password:
$ ls
example.proj
```
Then to work on the project again:
```
$ pappy -c example.proj
Enter a password:
Proxy is listening on port 8000
pappy> !ls
config.json data.db hello.txt
pappy>
```
Example of recovering after crash:
```
$ ls
crypt project.archive
$ pappy -c test.proj
Proxy is listening on port 8000
pappy> exit
Enter a password:
$ ls
test.proj
```
FAQ FAQ
--- ---
@ -1130,6 +1278,11 @@ Changelog
--------- ---------
The boring part of the readme The boring part of the readme
* 0.2.11
* Project directory compression/encryption. Thanks, onizenso!
* Add `submit` command
* Add macro templates
* Add header replacement and resubmit in-context requests macro templates
* 0.2.10 * 0.2.10
* Add wildcard support for requests that can take in multiple request ids * Add wildcard support for requests that can take in multiple request ids
* Update dump_response to dump multiple requests at the same time * Update dump_response to dump multiple requests at the same time

@ -35,8 +35,6 @@ Anyways, here's some ideas for things you could implement:
Make it easy to pass a request to SQLMap to check for SQLi. Make sure you can configure which fields you do/don't want tested and by default just give either "yes it looks like SQLi" or "no it doesn't look like SQLi" Make it easy to pass a request to SQLMap to check for SQLi. Make sure you can configure which fields you do/don't want tested and by default just give either "yes it looks like SQLi" or "no it doesn't look like SQLi"
* Additional macro templates * Additional macro templates
Write some commands for generating additional types of macros. For example let people generate an intercepting macro that does search/replace or modifies a header. Save as much typing as possible for common actions. Write some commands for generating additional types of macros. For example let people generate an intercepting macro that does search/replace or modifies a header. Save as much typing as possible for common actions.
* Show requests/responses real-time as they go through the proxy
Let people watch requests as they pass through the proxy. It's fine to implement this as an intercepting macro since people watching the requests aren't going to notice response streaming being disabled.
* Vim plugin to make editing HTTP messages easier * Vim plugin to make editing HTTP messages easier
Implement some functionality to make editing HTTP messages easier. It would be great to have a plugin to automatically add to vim when using the interceptor/repeater to make editing requests easier. Look at burp's request editor and try to implement anything you miss from it. Implement some functionality to make editing HTTP messages easier. It would be great to have a plugin to automatically add to vim when using the interceptor/repeater to make editing requests easier. Look at burp's request editor and try to implement anything you miss from it.
* Request Diff * Request Diff

@ -272,10 +272,10 @@ Using defer.inlineCallbacks With a Command
.. note:: .. note::
This tutorial won't tell you how to use inlineCallbacks in general. Type "twisted inline callbacks" into google to figure out what they are. This is mainly just a reminder to use the ``crochet`` wrapper for console commands and warning you that some functions may return deferreds that you may have to deal with. This tutorial won't tell you how to use inlineCallbacks in general. Type "twisted inline callbacks" into google to figure out what they are. This is mainly just a reminder to use the ``crochet`` wrapper for console commands and warning you that some functions may return deferreds that you may have to deal with.
Since you're writing a plugin, you'll probably be using functions which return a deferred. And to keep things readable, you'll want to use the ``defer.inlineCallbacks`` function wrapper. Unfortunately, you can't bind async functions to commands. Luckily, there's a library called `crochet <https://pypi.python.org/pypi/crochet>`_ which lets you add another wrapper to the function that lets it be used like a blocking function. Rather than talking about it, let's write a plugin to call :func:`pappyproxy.console.load_reqlist` to print out some requests' hosts. Let's start by pretending it's a normal function:: Since you're writing a plugin, you'll probably be using functions which return a deferred. And to keep things readable, you'll want to use the ``defer.inlineCallbacks`` function wrapper. Unfortunately, you can't bind async functions to commands. Luckily, there's a library called `crochet <https://pypi.python.org/pypi/crochet>`_ which lets you add another wrapper to the function that lets it be used like a blocking function. Rather than talking about it, let's write a plugin to call :func:`pappyproxy.util.load_reqlist` to print out some requests' hosts. Let's start by pretending it's a normal function::
import shlex import shlex
from pappyproxy.console import load_reqlist from pappyproxy.util import load_reqlist
def print_hosts(line): def print_hosts(line):
args = shlex.split(line) args = shlex.split(line)
@ -309,10 +309,10 @@ And we run it::
iteration over non-sequence iteration over non-sequence
pappy> pappy>
Iteration over a non-sequence? what? Well, :func:`pappyproxy.console.load_reqlist` doesn't actually return a list of requests. It returns a deferred which returns a list of requests. I'm not going into the details (look up some stuff on using inline callbacks with Twisted if you want more info), but the way to fix it is to slap an ``inlineCallbacks`` wrapper on the function and ``yield`` the result of the function. Now it looks like this:: Iteration over a non-sequence? what? Well, :func:`pappyproxy.util.load_reqlist` doesn't actually return a list of requests. It returns a deferred which returns a list of requests. I'm not going into the details (look up some stuff on using inline callbacks with Twisted if you want more info), but the way to fix it is to slap an ``inlineCallbacks`` wrapper on the function and ``yield`` the result of the function. Now it looks like this::
import shlex import shlex
from pappyproxy.console import load_reqlist from pappyproxy.util import load_reqlist
from twisted.internet import defer from twisted.internet import defer
@defer.inlineCallbacks @defer.inlineCallbacks
@ -336,7 +336,7 @@ However, the console assumes that any functions it calls will be blocking. As a
import shlex import shlex
import crochet import crochet
from pappyproxy.console import load_reqlist from pappyproxy.util import load_reqlist
from twisted.internet import defer from twisted.internet import defer
@crochet.wait_for(timeout=None) @crochet.wait_for(timeout=None)
@ -394,7 +394,7 @@ Here is an example plugin for storing the user-agent (if it exists) in the ``plu
import shlex import shlex
from twisted.internet import defer from twisted.internet import defer
from pappyproxy.console import load_reqlist from pappyproxy.util import load_reqlist
from pappyproxy.plugin import main_context from pappyproxy.plugin import main_context
from pappyproxy.util import PappyException from pappyproxy.util import PappyException
@ -435,8 +435,7 @@ Here is an example plugin for storing the user-agent (if it exists) in the ``plu
Useful Functions Useful Functions
---------------- ----------------
* Load a request by id: :func:`pappyproxy.http.Request.load_request` See :mod:`pappyproxy.plugin` and :mod:`pappyproxy.util` for useful functions
* Create a filter from a filter string: :func:`pappyproxy.context.Filter.from_filter_string`
Built In Plugins As Examples Built In Plugins As Examples
============================ ============================

@ -1 +1 @@
__version__ = '0.2.10' __version__ = '0.2.11'

@ -46,14 +46,14 @@ class PappyConfig(object):
:Default: None :Default: None
.. data: listeners .. data:: listeners
The list of active listeners. It is a list of tuples of the format (port, interface) The list of active listeners. It is a list of tuples of the format (port, interface)
Not modifiable after startup. Configured in the ``config.json`` file for the project. Not modifiable after startup. Configured in the ``config.json`` file for the project.
:Default: ``[(8000, '127.0.0.1')]`` :Default: ``[(8000, '127.0.0.1')]``
.. data: socks_proxy .. data:: socks_proxy
Details for a SOCKS proxy. It is a dict with the following key/values:: Details for a SOCKS proxy. It is a dict with the following key/values::
@ -66,7 +66,7 @@ class PappyConfig(object):
:Default: ``null`` :Default: ``null``
.. data: http_proxy .. data:: http_proxy
Details for an upstream HTTP proxy. It is a dict with the following key/values:: Details for an upstream HTTP proxy. It is a dict with the following key/values::
@ -77,37 +77,37 @@ class PappyConfig(object):
If null, no proxy will be used. If null, no proxy will be used.
.. data: plugin_dirs .. data:: plugin_dirs
List of directories that plugins are loaded from. Not modifiable. List of directories that plugins are loaded from. Not modifiable.
:Default: ``['{DATA_DIR}/plugins', '{PAPPY_DIR}/plugins']`` :Default: ``['{DATA_DIR}/plugins', '{PAPPY_DIR}/plugins']``
.. data: save_history .. data:: save_history
Whether command history should be saved to a file/loaded at startup. Whether command history should be saved to a file/loaded at startup.
:Default: True :Default: True
.. data: config_dict .. data:: config_dict
The dictionary read from config.json. When writing plugins, use this to load The dictionary read from config.json. When writing plugins, use this to load
configuration options for your plugin. configuration options for your plugin.
.. data: global_config_dict .. data:: global_config_dict
The dictionary from ~/.pappy/global_config.json. It contains settings for The dictionary from ~/.pappy/global_config.json. It contains settings for
Pappy that are specific to the current computer. Avoid putting settings here, Pappy that are specific to the current computer. Avoid putting settings here,
especially if it involves specific projects. especially if it involves specific projects.
.. data: archive .. data:: archive
Project archive compressed as a ``tar.bz2`` archive if libraries available on the system, Project archive compressed as a ``tar.bz2`` archive if libraries available on the system,
otherwise falls back to zip archive. otherwise falls back to zip archive.
:Default: ``project.archive`` :Default: ``project.archive``
.. data: crypt_dir .. data:: crypt_dir
Temporary working directory to unpack an encrypted project archive. Directory Temporary working directory to unpack an encrypted project archive. Directory
will contain copies of normal startup files, e.g. conifg.json, cmdhistory, etc. will contain copies of normal startup files, e.g. conifg.json, cmdhistory, etc.
@ -117,20 +117,20 @@ class PappyConfig(object):
:Default: ``crypt`` :Default: ``crypt``
.. data: crypt_file .. data:: crypt_file
Encrypted archive of the temporary working directory ``crypt_dir``. Compressed as a Encrypted archive of the temporary working directory ``crypt_dir``. Compressed as a
tar.bz2 archive if libraries available on the system, otherwise falls back to zip. tar.bz2 archive if libraries available on the system, otherwise falls back to zip.
:Default: ``project.crypt`` :Default: ``project.crypt``
.. data: crypt_session .. data:: crypt_session
Boolean variable to determine whether pappy started in crypto mode Boolean variable to determine whether pappy started in crypto mode
:Default: False :Default: False
.. data: salt_len .. data:: salt_len
Length of the nonce-salt value appended to the end of `crypt_file` Length of the nonce-salt value appended to the end of `crypt_file`

@ -129,6 +129,59 @@ def repeatable_parse_qs(s):
def request_by_id(reqid): def request_by_id(reqid):
req = yield Request.load_request(str(reqid)) req = yield Request.load_request(str(reqid))
defer.returnValue(req) defer.returnValue(req)
@defer.inlineCallbacks
def async_submit_requests(reqs, mangle=False, save=False):
"""
async_submit_requests(reqs, mangle=False)
:param mangle: Whether to pass the requests through intercepting macros
:type mangle: Bool
:rtype: DeferredList
Submits a list of requests at the same time asynchronously.
Responses/unmangled versions will be attached to the request objects in the list.
Prints progress to stdout.
"""
print 'Submitting %d request(s)' % len(reqs)
dones = 0
errors = 0
list_deferred = defer.Deferred()
deferreds = []
for r in reqs:
d = r.async_submit(mangle=mangle)
deferreds.append(d)
# Really not the best way to do this. If one request hangs forever the whole thing will
# just hang in the middle
for d in deferreds:
try:
yield d
dones += 1
except Exception as e:
errors += 1
print e
finished = dones+errors
if finished % 30 == 0 or finished == len(reqs):
if errors > 0:
print '{0}/{1} complete with {3} errors ({2:.2f}%)'.format(finished, len(reqs), (float(finished)/len(reqs))*100, errors)
else:
print '{0}/{1} complete ({2:.2f}%)'.format(finished, len(reqs), (float(finished)/len(reqs))*100)
if finished == len(reqs):
list_deferred.callback(None)
if save:
for r in reqs:
yield r.async_deep_save()
@crochet.wait_for(timeout=180.0)
@defer.inlineCallbacks
def submit_requests(*args, **kwargs):
ret = yield async_submit_requests(*args, **kwargs)
defer.returnValue(ret)
########## ##########
## Classes ## Classes

@ -7,9 +7,29 @@ import stat
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from pappyproxy.pappy import session from pappyproxy.pappy import session
from pappyproxy.util import PappyException from pappyproxy.util import PappyException, load_reqlist
from twisted.internet import defer from twisted.internet import defer
## Template generating functions
# Must be declared before MacroTemplate class
@defer.inlineCallbacks
def gen_template_args_macro(args):
if len(args) > 0:
reqids = args[0]
reqs = yield load_reqlist(reqids)
else:
reqs = []
defer.returnValue(macro_from_requests(reqs))
def gen_template_generator_noargs(name):
def f(args):
subs = {}
subs['macro_name'] = 'Macro %d' % random.randint(1,99999999)
subs['short_name'] = ''
return MacroTemplate.fill_template(name, subs)
return f
class Macro(object): class Macro(object):
""" """
A class representing a macro that can perform a series of requests and add A class representing a macro that can perform a series of requests and add
@ -196,6 +216,66 @@ class FileInterceptMacro(InterceptMacro):
rsp = yield self.source.async_mangle_response(request) rsp = yield self.source.async_mangle_response(request)
defer.returnValue(rsp) defer.returnValue(rsp)
defer.returnValue(request.response) defer.returnValue(request.response)
class MacroTemplate(object):
_template_data = {
'macro': ('macro.py.template',
'Generic macro template',
'[reqids]',
'macro_{fname}.py',
gen_template_args_macro),
'intmacro': ('intmacro.py.template',
'Generic intercepting macro template',
'',
'int_{fname}.py',
gen_template_generator_noargs('intmacro')),
'modheader': ('macro_header.py.template',
'Modify a header in the request and the response if it exists.',
'',
'int_{fname}.py',
gen_template_generator_noargs('modheader')),
'resubmit': ('macro_resubmit.py.template',
'Resubmit all in-context requests',
'',
'macro_{fname}.py',
gen_template_generator_noargs('resubmit')),
}
@classmethod
def fill_template(cls, template, subs):
loader = FileSystemLoader(session.config.pappy_dir+'/templates')
env = Environment(loader=loader)
template = env.get_template(cls._template_data[template][0])
return template.render(zip=zip, **subs)
@classmethod
@defer.inlineCallbacks
def fill_template_args(cls, template, args=[]):
ret = cls._template_data[template][4](args)
if isinstance(ret, defer.Deferred):
ret = yield ret
defer.returnValue(ret)
@classmethod
def template_filename(cls, template, fname):
return cls._template_data[template][3].format(fname=fname)
@classmethod
def template_list(cls):
return [k for k, v in cls._template_data.iteritems()]
@classmethod
def template_description(cls, template):
return cls._template_data[template][1]
@classmethod
def template_argstring(cls, template):
return cls._template_data[template][2]
## Other functions
def load_macros(loc): def load_macros(loc):
""" """
@ -279,26 +359,8 @@ def macro_from_requests(reqs, short_name='', long_name=''):
subs['req_lines'] = req_lines subs['req_lines'] = req_lines
subs['req_params'] = req_params subs['req_params'] = req_params
loader = FileSystemLoader(session.config.pappy_dir+'/templates') return MacroTemplate.fill_template('macro', subs)
env = Environment(loader=loader)
template = env.get_template('macro.py.template')
return template.render(zip=zip, **subs)
def gen_imacro(short_name='', long_name=''):
subs = {}
if long_name:
subs['macro_name'] = long_name
else:
random.seed()
subs['macro_name'] = 'Macro %d' % random.randint(1,99999999)
subs['short_name'] = short_name
loader = FileSystemLoader(session.config.pappy_dir+'/templates')
env = Environment(loader=loader)
template = env.get_template('intmacro.py.template')
return template.render(**subs)
@defer.inlineCallbacks @defer.inlineCallbacks
def mangle_request(request, intmacros): def mangle_request(request, intmacros):
""" """

@ -168,6 +168,18 @@ def main_context_ids(*args, **kwargs):
""" """
ret = yield async_main_context_ids(*args, **kwargs) ret = yield async_main_context_ids(*args, **kwargs)
defer.returnValue(ret) defer.returnValue(ret)
def add_to_history(req):
"""
Save a request to history without saving it to the data file. The request
will only be saved in memory, so when the program is exited or `clrmem`
is run, the request will be deleted.
:param req: The request to add to history
:type req: :class:`pappyproxy.http.Request`
"""
pappyproxy.http.Request.cache.add(req)
pappyproxy.context.reset_context_caches()
def run_cmd(cmd): def run_cmd(cmd):
""" """

@ -3,7 +3,7 @@ import pappyproxy
import shlex import shlex
from pappyproxy.plugin import active_intercepting_macros, add_intercepting_macro, remove_intercepting_macro from pappyproxy.plugin import active_intercepting_macros, add_intercepting_macro, remove_intercepting_macro
from pappyproxy.macros import load_macros, macro_from_requests, gen_imacro from pappyproxy.macros import load_macros, macro_from_requests, MacroTemplate
from pappyproxy.util import PappyException, load_reqlist, autocomplete_startswith from pappyproxy.util import PappyException, load_reqlist, autocomplete_startswith
from twisted.internet import defer from twisted.internet import defer
@ -12,6 +12,25 @@ loaded_int_macros = []
macro_dict = {} macro_dict = {}
int_macro_dict = {} int_macro_dict = {}
@defer.inlineCallbacks
def gen_macro_helper(line, template=None):
args = shlex.split(line)
if template is None:
fname = args[0]
template_name = args[1]
argstart = 2
else:
fname = args[0]
template_name = template
argstart = 1
if template_name not in MacroTemplate.template_list():
raise PappyException('%s is not a valid template name' % template_name)
script_str = yield MacroTemplate.fill_template_args(template_name, args[argstart:])
fname = MacroTemplate.template_filename(template_name, fname)
with open(fname, 'wc') as f:
f.write(script_str)
print 'Wrote script to %s' % fname
def load_macros_cmd(line): def load_macros_cmd(line):
""" """
Load macros from a directory. By default loads macros in the current directory. Load macros from a directory. By default loads macros in the current directory.
@ -193,34 +212,37 @@ def generate_macro(line):
Generate a macro script with request objects Generate a macro script with request objects
Usage: generate_macro <name> [reqs] Usage: generate_macro <name> [reqs]
""" """
if line == '': yield gen_macro_helper(line, template='macro')
raise PappyException('Macro name is required')
args = shlex.split(line)
name = args[0]
if len(args) > 1:
reqs = yield load_reqlist(args[1])
else:
reqs = []
script_str = macro_from_requests(reqs)
fname = 'macro_%s.py' % name
with open(fname, 'wc') as f:
f.write(script_str)
print 'Wrote script to %s' % fname
@crochet.wait_for(timeout=None)
@defer.inlineCallbacks
def generate_int_macro(line): def generate_int_macro(line):
""" """
Generate an intercepting macro script Generate an intercepting macro script
Usage: generate_int_macro <name> Usage: generate_int_macro <name>
""" """
yield gen_macro_helper(line, template='intmacro')
@crochet.wait_for(timeout=None)
@defer.inlineCallbacks
def generate_template_macro(line):
"""
Generate a macro from a built in template
Usage: generate_template_macro <fname> <template> [args]
"""
if line == '': if line == '':
raise PappyException('Macro name is required') print 'Usage: gtma <fname> <template> [args]'
args = shlex.split(line) print 'Macro templates:'
name = args[0]
script_str = gen_imacro() templates = MacroTemplate.template_list()
fname = 'int_%s.py' % name templates.sort()
with open(fname, 'wc') as f: for t in templates:
f.write(script_str) if MacroTemplate.template_argstring(t):
print 'Wrote script to %s' % fname print '"%s %s" - %s' % (t, MacroTemplate.template_argstring(t), MacroTemplate.template_description(t))
else:
print '"%s" - %s' % (t, MacroTemplate.template_description(t))
else:
yield gen_macro_helper(line)
@crochet.wait_for(timeout=None) @crochet.wait_for(timeout=None)
@defer.inlineCallbacks @defer.inlineCallbacks
@ -241,6 +263,7 @@ def load_cmds(cmd):
'rpy': (rpy, None), 'rpy': (rpy, None),
'generate_int_macro': (generate_int_macro, None), 'generate_int_macro': (generate_int_macro, None),
'generate_macro': (generate_macro, None), 'generate_macro': (generate_macro, None),
'generate_template_macro': (generate_template_macro, None),
'list_int_macros': (list_int_macros, None), 'list_int_macros': (list_int_macros, None),
'stop_int_macro': (stop_int_macro, complete_stop_int_macro), 'stop_int_macro': (stop_int_macro, complete_stop_int_macro),
'run_int_macro': (run_int_macro, complete_run_int_macro), 'run_int_macro': (run_int_macro, complete_run_int_macro),
@ -251,6 +274,7 @@ def load_cmds(cmd):
#('rpy', ''), #('rpy', ''),
('generate_int_macro', 'gima'), ('generate_int_macro', 'gima'),
('generate_macro', 'gma'), ('generate_macro', 'gma'),
('generate_template_macro', 'gtma'),
('list_int_macros', 'lsim'), ('list_int_macros', 'lsim'),
('stop_int_macro', 'sim'), ('stop_int_macro', 'sim'),
('run_int_macro', 'rim'), ('run_int_macro', 'rim'),

@ -1,3 +1,4 @@
import argparse
import crochet import crochet
import pappyproxy import pappyproxy
import shlex import shlex
@ -7,8 +8,10 @@ from pappyproxy.colors import Colors, Styles, path_formatter, host_color, scode_
from pappyproxy.util import PappyException, remove_color, confirm, load_reqlist, Capturing from pappyproxy.util import PappyException, remove_color, confirm, load_reqlist, Capturing
from pappyproxy.macros import InterceptMacro from pappyproxy.macros import InterceptMacro
from pappyproxy.requestcache import RequestCache from pappyproxy.requestcache import RequestCache
from pappyproxy.session import Session
from pappyproxy.pappy import session from pappyproxy.pappy import session
from pappyproxy.plugin import add_intercepting_macro, remove_intercepting_macro from pappyproxy.plugin import add_intercepting_macro, remove_intercepting_macro, add_to_history
from pappyproxy.http import async_submit_requests, Request
from twisted.internet import defer from twisted.internet import defer
from twisted.enterprise import adbapi from twisted.enterprise import adbapi
@ -190,6 +193,77 @@ def run_without_color(line):
def version(line): def version(line):
import pappyproxy import pappyproxy
print pappyproxy.__version__ print pappyproxy.__version__
@crochet.wait_for(timeout=180.0)
@defer.inlineCallbacks
def submit(line):
"""
Resubmit some requests, optionally with modified headers and cookies.
Usage: submit reqids [-h] [-m] [-u] [-p] [-c [COOKIES [COOKIES ...]]] [-d [HEADERS [HEADERS ...]]]
"""
parser = argparse.ArgumentParser(prog="submit", usage=submit.__doc__)
parser.add_argument('reqids')
parser.add_argument('-m', '--inmem', action='store_true', help='Store resubmitted requests in memory without storing them in the data file')
parser.add_argument('-u', '--unique', action='store_true', help='Only resubmit one request per endpoint (different URL parameters are different endpoints)')
parser.add_argument('-p', '--uniquepath', action='store_true', help='Only resubmit one request per endpoint (ignoring URL parameters)')
parser.add_argument('-c', '--cookies', nargs='*', help='Apply a cookie to requests before submitting')
parser.add_argument('-d', '--headers', nargs='*', help='Apply a header to requests before submitting')
args = parser.parse_args(shlex.split(line))
headers = {}
cookies = {}
if args.headers:
for h in args.headers:
k, v = h.split('=', 1)
headers[k] = v
if args.cookies:
for c in args.cookies:
k, v = c.split('=', 1)
cookies[k] = v
if args.unique and args.uniquepath:
raise PappyException('Both -u and -p cannot be given as arguments')
newsession = Session(cookie_vals=cookies, header_vals=headers)
reqs = yield load_reqlist(args.reqids)
if args.unique or args.uniquepath:
endpoints = set()
new_reqs = []
for r in reqs:
if args.unique:
s = r.url
else:
s = r.path
if not s in endpoints:
new_reqs.append(r.copy())
endpoints.add(s)
reqs = new_reqs
else:
reqs = [r.copy() for r in reqs]
for req in reqs:
newsession.apply_req(req)
conf_message = "You're about to submit %d requests, continue?" % len(reqs)
if not confirm(conf_message):
defer.returnValue(None)
for r in reqs:
r.tags.add('resubmitted')
if args.inmem:
yield async_submit_requests(reqs)
for req in reqs:
add_to_history(req)
else:
yield async_submit_requests(reqs, save=True)
def load_cmds(cmd): def load_cmds(cmd):
cmd.set_cmds({ cmd.set_cmds({
@ -202,6 +276,7 @@ def load_cmds(cmd):
'nocolor': (run_without_color, None), 'nocolor': (run_without_color, None),
'watch': (watch_proxy, None), 'watch': (watch_proxy, None),
'version': (version, None), 'version': (version, None),
'submit': (submit, None)
}) })
cmd.add_aliases([ cmd.add_aliases([
#('rpy', ''), #('rpy', ''),

@ -14,6 +14,7 @@ from pappyproxy.plugin import async_main_context_ids
from pappyproxy.colors import Colors, Styles, verb_color, scode_color, path_formatter, host_color from pappyproxy.colors import Colors, Styles, verb_color, scode_color, path_formatter, host_color
from pygments.formatters import TerminalFormatter from pygments.formatters import TerminalFormatter
from pygments.lexers.data import JsonLexer from pygments.lexers.data import JsonLexer
from pygments.lexers.html import XmlLexer
################### ###################
## Helper functions ## Helper functions
@ -103,6 +104,8 @@ def guess_pretty_print_fmt(msg):
return 'json' return 'json'
elif 'www-form' in msg.headers['content-type']: elif 'www-form' in msg.headers['content-type']:
return 'form' return 'form'
elif 'application/xml' in msg.headers['content-type']:
return 'xml'
return 'text' return 'text'
def pretty_print_body(fmt, body): def pretty_print_body(fmt, body):
@ -121,6 +124,10 @@ def pretty_print_body(fmt, body):
print s print s
elif fmt.lower() == 'text': elif fmt.lower() == 'text':
print body print body
elif fmt.lower() == 'xml':
import xml.dom.minidom
xml = xml.dom.minidom.parseString(body)
print pygments.highlight(xml.toprettyxml(), XmlLexer(), TerminalFormatter())
else: else:
raise PappyException('"%s" is not a valid format' % fmt) raise PappyException('"%s" is not a valid format' % fmt)
except PappyException as e: except PappyException as e:

@ -34,7 +34,7 @@ class Session(object):
if k not in self.headers: if k not in self.headers:
self.headers.append(k) self.headers.append(k)
def _cookie_obj(k, v): def _cookie_obj(self, k, v):
""" """
Returns the value as a cookie object regardless of if the cookie is a string or a ResponseCookie. Returns the value as a cookie object regardless of if the cookie is a string or a ResponseCookie.
""" """
@ -44,7 +44,7 @@ class Session(object):
cookie_str = '%s=%s' % (k, v) cookie_str = '%s=%s' % (k, v)
return ResponseCookie(cookie_str) return ResponseCookie(cookie_str)
def _cookie_val(v): def _cookie_val(self, v):
""" """
Returns the value of the cookie regardless of if the value is a string or a ResponseCookie Returns the value of the cookie regardless of if the value is a string or a ResponseCookie
""" """
@ -76,7 +76,7 @@ class Session(object):
""" """
for k, v in self.cookie_vals.iteritems(): for k, v in self.cookie_vals.iteritems():
val = self._cookie_obj(v) val = self._cookie_obj(k, v)
rsp.set_cookie(val) rsp.set_cookie(val)
# Don't apply headers to responses # Don't apply headers to responses
@ -115,13 +115,14 @@ class Session(object):
if header in self.headers: if header in self.headers:
self.header_vals[header] = req.headers[header] self.header_vals[header] = req.headers[header]
def save_rsp(self, rsp, cookies=None): def save_rsp(self, rsp, cookies=None, save_all=False):
""" """
save_rsp(rsp, cookies=None) save_rsp(rsp, cookies=None)
Update the state of the session from the response. Only cookies can be Update the state of the session from the response. Only cookies can be
updated from a response. Additional values can be added to the whitelist updated from a response. Additional values can be added to the whitelist
by passing in a list of values for the ``cookies`` parameter. by passing in a list of values for the ``cookies`` parameter. If save_all
is given, all set cookies will be added to the session.
""" """
if cookies: if cookies:
for c in cookies: for c in cookies:
@ -136,7 +137,11 @@ class Session(object):
self.cookie_vals[cookie] = rsp.cookies[cookie] self.cookie_vals[cookie] = rsp.cookies[cookie]
else: else:
for k, v in rsp.cookies.all_pairs(): for k, v in rsp.cookies.all_pairs():
if v.key in self.cookies: if save_all:
self.cookie_vals[v.key] = v
if not v.key in self.cookies:
self.cookies.append(v.key)
elif v.key in self.cookies:
self.cookie_vals[v.key] = v self.cookie_vals[v.key] = v
def set_cookie(key, val): def set_cookie(key, val):
@ -171,5 +176,5 @@ class Session(object):
if not key in self.cookie_vals: if not key in self.cookie_vals:
raise KeyError('Cookie is not stored in session.') raise KeyError('Cookie is not stored in session.')
v = self.cookie_vals[key] v = self.cookie_vals[key]
return self._cookie_obj(v) return self._cookie_obj(key, v)

@ -1,7 +1,4 @@
from pappyproxy.http import Request, get_request, post_request, request_by_id {% include 'macroheader.py.template' %}
from pappyproxy.plugin import main_context_ids
from pappyproxy.context import set_tag
from pappyproxy.iter import *
## Iterator cheat sheet: ## Iterator cheat sheet:
# fuzz_path_trav() - Values for fuzzing path traversal # fuzz_path_trav() - Values for fuzzing path traversal
@ -11,8 +8,6 @@ from pappyproxy.iter import *
# common_usernames() - Common usernames # common_usernames() - Common usernames
# fuzz_dirs() - Common web paths (ie /wp-admin) # fuzz_dirs() - Common web paths (ie /wp-admin)
MACRO_NAME = '{{macro_name}}'
SHORT_NAME = '{{short_name}}'
{% if req_lines %} {% if req_lines %}
########### ###########
## Requests ## Requests

@ -0,0 +1,27 @@
from pappyproxy.session import Session
MACRO_NAME = '{{macro_name}}'
SHORT_NAME = '{{short_name}}'
runargs = []
def init(args):
global runargs
runargs = args
def modify_header(msg, key, val):
"""
Modifies the header in a request or a response if it already exists in
the message
"""
if key in msg.headers:
msg.headers[key] = val
def mangle_request(request):
global runargs
modify_header(request, 'headername', 'headerval')
return request
def mangle_response(request):
global runargs
modify_header(request.response, 'headername', 'headerval')
return request.response

@ -0,0 +1,34 @@
import sys
{% include 'macroheader.py.template' %}
def run_macro(args):
# Get IDs of in-context requests
reqids = main_context_ids()
reqids.reverse() # Resubmit earliest first
reqs = []
# Create session jar (uncomment jar functions to use)
#jar = Session() # Create a cookie jar
# Iterate over each request and submit it
for rid in reqids:
print rid,
sys.stdout.flush()
r = request_by_id(rid)
r = r.copy()
#jar.apply_req(r) # Apply headers/cookies from the cookie jar
#####################
# Modify request here
r.submit()
#jar.save_rsp(r.response, save_all=True) # Update the cookie jar from the response
#r.save() # Save the request to the data file
reqs.append(r)
print ''
# Store the requests in memory
set_tag('resubmit', reqs)

@ -0,0 +1,8 @@
from pappyproxy.http import Request, get_request, post_request, request_by_id
from pappyproxy.plugin import main_context_ids
from pappyproxy.context import set_tag
from pappyproxy.session import Session
from pappyproxy.iter import *
MACRO_NAME = '{{macro_name}}'
SHORT_NAME = '{{short_name}}'

@ -48,14 +48,14 @@ def test_session_cookieobj_basic(req, rsp):
assert req.headers['auth'] == 'bar' assert req.headers['auth'] == 'bar'
assert 'auth' not in rsp.headers assert 'auth' not in rsp.headers
def test_session_get_req(req): def test_session_save_req(req):
req.headers['BasicAuth'] = 'asdfasdf' req.headers['BasicAuth'] = 'asdfasdf'
req.headers['Host'] = 'www.myfavoritecolor.foobar' req.headers['Host'] = 'www.myfavoritecolor.foobar'
req.cookies['session'] = 'foobar' req.cookies['session'] = 'foobar'
req.cookies['favorite_color'] = 'blue' req.cookies['favorite_color'] = 'blue'
s = Session() s = Session()
s.get_req(req, ['session'], ['BasicAuth']) s.save_req(req, ['session'], ['BasicAuth'])
assert s.cookies == ['session'] assert s.cookies == ['session']
assert s.headers == ['BasicAuth'] assert s.headers == ['BasicAuth']
assert s.cookie_vals['session'].val == 'foobar' assert s.cookie_vals['session'].val == 'foobar'
@ -63,14 +63,14 @@ def test_session_get_req(req):
assert 'Host' not in s.headers assert 'Host' not in s.headers
assert 'favorite_color' not in s.cookies assert 'favorite_color' not in s.cookies
def test_session_get_rsp(rsp): def test_session_save_rsp(rsp):
rsp.headers['BasicAuth'] = 'asdfasdf' rsp.headers['BasicAuth'] = 'asdfasdf'
rsp.headers['Host'] = 'www.myfavoritecolor.foobar' rsp.headers['Host'] = 'www.myfavoritecolor.foobar'
rsp.set_cookie(ResponseCookie('session=foobar; secure; path=/')) rsp.set_cookie(ResponseCookie('session=foobar; secure; path=/'))
rsp.set_cookie(ResponseCookie('favorite_color=blue; secure; path=/')) rsp.set_cookie(ResponseCookie('favorite_color=blue; secure; path=/'))
s = Session() s = Session()
s.get_rsp(rsp, ['session']) s.save_rsp(rsp, ['session'])
assert s.cookies == ['session'] assert s.cookies == ['session']
assert s.headers == [] assert s.headers == []
assert s.cookie_vals['session'].key == 'session' assert s.cookie_vals['session'].key == 'session'
@ -99,6 +99,21 @@ def test_session_mixed(req, rsp):
r.start_line = 'HTTP/1.1 200 OK' r.start_line = 'HTTP/1.1 200 OK'
r.set_cookie(ResponseCookie('state=bazzers')) r.set_cookie(ResponseCookie('state=bazzers'))
r.set_cookie(ResponseCookie('session=buzzers')) r.set_cookie(ResponseCookie('session=buzzers'))
s.get_rsp(r) s.save_rsp(r)
assert s.cookie_vals['session'].val == 'buzzers' assert s.cookie_vals['session'].val == 'buzzers'
assert s.cookie_vals['state'].val == 'bazzers' assert s.cookie_vals['state'].val == 'bazzers'
def test_session_save_all(req, rsp):
s = Session()
rsp.set_cookie(ResponseCookie('state=bazzers'))
rsp.set_cookie(ResponseCookie('session=buzzers'))
s.save_rsp(rsp, save_all=True)
assert s.cookies == ['state', 'session']
assert not 'state' in req.cookies
assert not 'session' in req.cookies
s.apply_req(req)
assert req.cookies['state'] == 'bazzers'
assert req.cookies['session'] == 'buzzers'

Loading…
Cancel
Save