Repository: GDSSecurity/GWT-Penetration-Testing-Toolset
Branch: master
Commit: f27a5aa2b0f3
Files: 10
Total size: 57.3 KB
Directory structure:
gitextract__7fywc2q/
├── README
├── gwtenum/
│ ├── README
│ └── gwtenum.py
├── gwtfuzzer/
│ ├── README
│ └── gwtfuzzer.py
└── gwtparse/
├── GWTParser.py
├── Parameter.py
├── README
├── __init__.py
└── gwtparse.py
================================================
FILE CONTENTS
================================================
================================================
FILE: README
================================================
================================================
FILE: gwtenum/README
================================================
EXAMPLES
python gwt_rpc_enum.py -u "http://www.whirled.com/gwt/frame/frame.nocache.js"
python gwt_rpc_enum.py -u "https://adwords.google.com/billing/ui/accountcancel/account_cancel_widget.nocache.js"
================================================
FILE: gwtenum/gwtenum.py
================================================
#!/usr/bin/env python
'''
GwtEnum v0.2
Copyright (C) 2010 Ron Gutierrez
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
'''
import urllib2
import re
import pprint
import base64
import getpass
from optparse import OptionParser
desc = "A tool for enumerating GWT RPC methods"
methods = []
proxy_url = ""
basic_auth_encoded = ""
def get_global_val( varname, html_file ):
for html_line in html_file:
match = re.match( ".*," + re.escape(varname) +
"\=\'([A-Za-z0-9_\.\!\@\#\$\%\^\&\*\(\)"
"\-\+\=\:\;\"\|\\\\/\?\>\,\<\~\`]+)\',", html_line )
if not match is None:
return match.group(1)
if __name__ == "__main__":
parser = OptionParser( usage="usage: %prog [options]",
description=desc,
version='%prog 0.10' )
parser.add_option('-p', '--proxy',
help="Proxy Host and Port (ie. -p \"http://proxy.internet.net:8080\")",
action="store" )
parser.add_option('-b', '--basicauth',
help="User Basic Authentication ( Will be prompted for creds )",
action="store_true" )
parser.add_option('-k', '--cookies',
help="Cookies to use when requesting the GWT Javascript Files (ie. -c \"JSESSIONID=AAAAAA\")",
action="store")
parser.add_option('-u', '--url',
help="Required: GWT Application Entrypoint Javascript File (ie. *.nocache.js )",
action="store")
(options, args) = parser.parse_args()
if options.url is None:
print( "\nMissing URL\n" )
parser.print_help()
exit()
url = options.url
gwt_docroot = '/'.join(url.split('/')[:-1])+'/'
req = urllib2.Request(url)
handlers = [ urllib2.HTTPHandler() ]
if url.startswith( "https://" ):
try:
import ssl
except ImportError:
print "SSL support not installed - exiting"
exit()
handlers.append( urllib2.HTTPSHandler() )
if options.proxy:
handlers.append( urllib2.ProxyHandler( {'http':'http://'+options.proxy}) )
opener = urllib2.build_opener(*handlers)
urllib2.install_opener( opener )
if options.basicauth:
username = raw_input( "Basic Auth Username: " )
password = getpass.getpass( "Basic Auth Password: " )
basic_auth_encoded = base64.encodestring( '%s:%s' % (username, password) ).strip()
req.add_header( "Authorization", "Basic %s" % basic_auth_encoded )
if options.cookies:
req.add_header( "Cookie", options.cookies )
response = urllib2.urlopen(req)
the_page = response.read()
html_files = re.findall( "([A-Z0-9]{30,35})", the_page )
if html_files is None:
print( "\nNo Cached HTML Files found\n" )
exit()
all_rpc_files = []
how_many_html_files_to_read = 1
for html_file in html_files:
if how_many_html_files_to_read == 0:
break
how_many_html_files_to_read -= 1
async_error_mess = ""
invoke_method = ""
cache_html = "%s%s.cache.html" % (gwt_docroot, html_file )
print( "Analyzing %s" % cache_html )
req = urllib2.Request( cache_html )
if options.cookies:
req.add_header( "Cookie", options.cookies )
if options.basicauth:
req.add_header( "Authorization", "Basic %s" % basic_auth_encoded )
try:
response = urllib2.urlopen(req)
except urllib2.HTTPError:
print( "404: Failed to Retrieve %s" % cache_html )
continue
the_page = response.readlines()
for line in the_page:
# Service and Method name Enumeration
rpc_method_match = re.match( "^function \w+\(.*method\:([A-Za-z0-9_\$]+),.*$", line )
if rpc_method_match:
if rpc_method_match.group(1) == "a":
continue
rpc_js_function = rpc_method_match.group(0).split(';')
service_and_method = ""
method_name = get_global_val( rpc_method_match.group(1), the_page )
if method_name is None:
continue
methods.append( "%s( " % method_name.replace( '_Proxy.', '.' ) )
# Parameter Enumeration
for i in range(0, len(rpc_js_function)):
try_match = re.match( "^try{.*$", rpc_js_function[i] )
if try_match:
i += 1
func_match = re.match( "^([A-Za-z0-9_\$]+)\(.*", rpc_js_function[i] )
payload_function = ""
if func_match:
payload_function = func_match.group(1)
i += 1
param_match = re.match( "^"+re.escape(payload_function)+
"\([A-Za-z0-9_\$]+\.[A-Za-z0-9_\$]+,([A-Za-z0-9_\$]+)\)",
rpc_js_function[i] )
num_of_params = 0
if param_match:
num_of_params = int(get_global_val( param_match.group(1), the_page ))
for j in range( 0, num_of_params ):
i += 1
param_var_match = re.match( "^"+re.escape(payload_function)+
"\([A-Za-z0-9_\$]+\.[A-Za-z0-9_\$]+,[A-Za-z0-9_\$]+\+"
"[A-Za-z0-9_\$]+\([A-Za-z0-9_\$]+,([A-Za-z0-9_\$]+)\)\)$",
rpc_js_function[i] )
if param_var_match:
param = get_global_val( param_var_match.group(1), the_page )
methods[-1] = methods[-1]+param+","
a_method = methods[-1][:-1]
methods[-1] = a_method + " )"
break
line_decor = "\n===========================\n"
print( "\n%sEnumerated Methods%s" % ( line_decor, line_decor ) )
methods = sorted(list(set(methods))) #uniq
for method in methods:
print( method )
print( "\n\n" )
================================================
FILE: gwtfuzzer/README
================================================
GwtFuzzer v0.1
This is a proof of concept fuzzer which was presented at OWASP Appsec DC. This script
utilizes the GDS Burp API and GWTParser class in order to filter out GWT RPC requests
from a Burp Proxy log and programmatically fuzz user-input. The presentation can be
downloaded at http://www.owasp.org/images/7/77/Attacking_Google_Web_Toolkit.ppt.
GDS Burp API can be downloaded here: http://mwielgoszewski.github.com/burpee/
NOTE:
Both Libraries must be added to your PYTHONPATH.
================================================
FILE: gwtfuzzer/gwtfuzzer.py
================================================
# -*- coding: utf-8 -*-
#!/usr/bin/env python
"""
GwtFuzzer v0.1
Copyright (C) 2010 Ron Gutierrez
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
import urllib2
import urllib
import os
import os.path
import time
from GWTParser import GWTParser
from gds.pub.burp import parse
from itertools import product
from optparse import OptionParser
'''
Globals
'''
attacklog = []
param_manip_log = []
def replay( burpobj, fuzzified, attackstr, gwtparsed, log ):
global options
headers = burpobj.get_request_headers()
if options.cookies is not None:
headers["Cookie"] = options.cookies
headers["Content-Length"] = str(len(fuzzified))
req = urllib2.Request(burpobj.url.geturl(), fuzzified, headers )
handlers = [ urllib2.HTTPHandler() ]
if options.proxy is not None:
handlers.append( urllib2.ProxyHandler( {'http':options.proxy} ) )
opener = urllib2.build_opener(*handlers)
urllib2.install_opener( opener )
errors_found = []
try:
resp = urllib2.urlopen(req)
data = resp.read()
# Check for error messages
errors_found = check_errors(data)
# Check to see if a exception message was returned
if is_exception(data):
errors_found.append( "GWT Exception Returned" )
# Success
log.append( { 'method':gwtparsed.rpc_deserialized[2]+"."+gwtparsed.rpc_deserialized[3],
'request_url':burpobj.url.geturl(),
'request_headers':headers,
'request_payload':fuzzified,
'attack': attackstr,
'response_status':200,
'response_content':data,
'response_size':len(data),
'errors_found':errors_found })
except urllib2.HTTPError, e:
# Request did not return a 200 OK
log.append( {'method':gwtparsed.rpc_deserialized[2]+"."+gwtparsed.rpc_deserialized[3],
'request_url':burpobj.url.geturl(),
'request_headers':headers,
'request_payload':fuzzified,
'attack': attackstr,
'response_status':e.code,
'response_content':e.read(),
'response_size':len(e.read()),
'errors_found':errors_found })
except urllib2.URLError, e:
# Host could not be reached
log.append( {'method':gwtparsed.rpc_deserialized[2]+"."+gwtparsed.rpc_deserialized[3],
'request_url':burpobj.url.geturl(),
'request_headers':headers,
'request_payload':fuzzified,
'attack': attackstr,
'response_status':'Python URLError Exception',
'response_content':e.reason(),
'response_size':0,
'errors_found':errors_found})
print( "Request failed: "+burpobj.url.geturl()+"("+e.reason+")" )
def check_errors( data ):
global errors
found = []
for error in errors:
if data.find( error ) != -1:
found.append( error )
print( "found "+error )
return found
def is_exception( data ):
if data.find( "//EX[", 0, 8 ) != -1:
return True
return False
def escape( str ):
return str.replace( '<', '<' ).replace( '>', '>' ).replace( '"', '"' ).replace( '\'', ''')
def filter_gwt_reqs( parsed ):
filtered = []
for burpobj in parsed:
headers = burpobj.get_request_headers()
if "Content-Type" in headers:
if headers["Content-Type"].find("text/x-gwt-rpc") != -1:
filtered.append( burpobj )
return filtered
def get_number_range( num ):
if num < options.idrange:
return 0, num+options.idrange
begin = int(num)-int(options.idrange)
end = int(num)+int(options.idrange)
return begin, end
def load_strings( list, filename ):
if os.path.exists( filename ):
f = open( filename, 'r' )
for line in f:
if line.find( "# ", 0, 2 ) == -1: # Ignore FuzzDB comments
list.append( line.strip() )
f.close()
else:
print( "Error: "+filename+" does not exist" )
exit()
def fuzz( burpobj ):
global options, attacks, attacklog, param_manip_log
# Parse the gwt string
gwtparsed = GWTParser()
gwtparsed.deserialize( burpobj.get_request_body() )
gwtlist = burpobj.get_request_body().split('|')
# This is where the magic happens.. Special Thanks to Marcin W.
# Test all GWT requests using the attack strings submitted
for( idx, param ), fuzzy in product( enumerate(gwtlist), attacks ):
# Check to see if index was marked as a fuzzible string value by GWTParse
if idx in gwtparsed.fuzzmarked and gwtparsed.fuzzmarked[idx] == "%s":
fuzzified = "%s|%s|%s" %('|'.join(gwtlist[:idx]), fuzzy.replace('|','\!'), '|'.join(gwtlist[idx+1:]))
replay( burpobj, fuzzified, fuzzy, gwtparsed, attacklog ) # Submit the request
# Test all GWT request for Parameter Manipulation
for idx, param in enumerate( gwtlist ):
if idx in gwtparsed.fuzzmarked and gwtparsed.fuzzmarked[idx] == "%d":
begin, end = get_number_range( param )
for i in range( int(begin), int(end) ):
fuzzified = "%s|%s|%s" %('|'.join(gwtlist[:idx]), str(i), '|'.join(gwtlist[idx+1:]))
replay( burpobj, fuzzified, str(i), gwtparsed, param_manip_log ) #Submit the request
def reportfuzz( logdir ):
global attacklog
f = open( logdir+"//gwtfuzz.html", 'w' )
f.write( '''
GWTFuzz Results
Fuzz Results
| ID
| Endpoint URL |
RPC Method |
Attack |
Request Data |
Resp Status |
Resp Size |
Resp Content |
Errors Found |
''' )
for idx, entry in enumerate(attacklog):
if len(entry['errors_found']) > 0:
f.write( '' )
elif entry['response_status'] != 200:
f.write( '
' )
else:
f.write( '
' )
f.write( '| '+str(idx)+' | ' +
''+escape(entry['request_url'])+' | ' +
''+escape(entry['method'])+' | ' +
''+escape(entry['attack'])+' | ' +
''+escape(entry['request_payload'])+' | ' +
''+str(entry['response_status'])+' | ' +
''+str(entry['response_size'])+' | ' +
'View Response | ' +
'' )
errorstr = ""
for error in entry['errors_found']:
errorstr = errorstr + escape(error) + ", "
errorstr = errorstr[:-2]
f.write( errorstr+' |
' )
# Write the HTTP response into a text file
f2 = open( logdir+'//responses/'+str(idx)+'.txt', 'w' )
f2.write( entry['response_content'] )
f2.close()
f.write( '
' )
f.close()
print( "Results saved to "+logdir )
def reportparam( logdir ):
global param_manip_log
f = open( logdir+"//param_manip.html", 'w' )
f.write( '''
GWT Parameter Manipulation Results
GWT Parameter Manipulation Results
| ID
| Endpoint URL |
RPC Method |
Attack |
Request Data |
Resp Status |
Resp Size |
Resp Content |
''' )
for idx, entry in enumerate(param_manip_log):
f.write( '| '+str(idx)+' | ' +
''+escape(entry['request_url'])+' | ' +
''+escape(entry['method'])+' | ' +
''+escape(entry['attack'])+' | ' +
''+escape(entry['request_payload'])+' | ' +
''+str(entry['response_status'])+' | ' +
''+str(entry['response_size'])+' | ' +
'View Response |
' +
'' )
# Write the HTTP response into a text file
f2 = open( logdir+'//responses//p'+str(idx)+'.txt', 'w' )
f2.write( entry['response_content'] )
f2.close()
f.write( ' |
' )
f.close()
if __name__ == "__main__":
global options, attacks, errors
attacks = []
errors = []
parser = OptionParser( usage="usage: %prog [options]",
description='Automates the fuzzing of GWT RPC requests',
version='%prog 0.10' )
parser.add_option('-b', '--burp',
help='Burp logfile to fuzz',
action='store' )
parser.add_option('-f', '--fuzzfile',
help='File containing attack strings',
action='store' )
parser.add_option('-e', '--errorfile',
help='File containing error messages',
action='store' )
parser.add_option('-o', '--output',
help='Directory to store results',
action='store' )
parser.add_option('-k', '--cookies',
help='Cookies to use when requesting GWT RPC pages',
action='store' )
parser.add_option('-p', '--proxy',
help='Proxy Host and Port (e.g. -p "http://proxy.internet.net:8080"',
action='store' )
parser.add_option('-i', '--idrange',
help='Range of decrements and increments to test parameter manipulation with',
action='store' )
(options, args) = parser.parse_args()
if options.burp is None:
print( "\nError: Missing Burp log file\n" )
parser.print_help()
exit()
elif options.fuzzfile is None:
print( "\nError: Missing fuzz file\n" )
parser.print_help()
exit()
if options.idrange and options.idrange < 1:
options.idrange = 100
print( "Invalid ID Range Entered: ID Range has been set to 100\n" )
elif options.idrange is None:
options.idrange = 100
print( "ID Range for Parameter Manipulation Testing has been set to 100\n" )
parsed = None
# Parse the Burp log using the GDS Burp API
if os.path.exists( options.burp ):
print( "Parsing Burp logfile" )
parsed = parse( options.burp )
else:
print( "\nBurp log file entered does not exist\n" )
exit()
logdir = ""
if options.output and os.path.exists( options.output ):
print( "Error: Output directory already exists." )
exit()
elif options.output:
logdir = options.output
else:
logdir = "gwtfuzz_results"+time.strftime("%Y%m%d%H%M%S")
os.mkdir( logdir )
os.mkdir( logdir+"//responses" )
if options.fuzzfile:
load_strings(attacks, options.fuzzfile)
if options.errorfile:
load_strings(errors, options.errorfile)
# Filter out the GWT RPC Requests from the log
filtered = filter_gwt_reqs(parsed)
print( "Fuzzing has commenced" )
# Fuzz each GWT RPC Request
for burpobj in filtered:
fuzz( burpobj )
# Generate Parameter Manipulation Report
reportparam( logdir )
reportfuzz( logdir )
================================================
FILE: gwtparse/GWTParser.py
================================================
# -*- coding: utf-8 -*-
#!/usr/bin/env python
"""
GwtParse v0.2
Copyright (C) 2010 Ron Gutierrez
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
import sys
import re
import pprint
from Parameter import Parameter
reload(sys)
sys.setdefaultencoding('utf-8')
#################################################
#
# Java Primitive Types and Object Wrappers
#
#################################################
FUZZ_STRING = "%s"
FUZZ_DIGIT = "%d"
STRING_OBJECT = "java.lang.String"
INTEGER_OBJECT = "java.lang.Integer"
DOUBLE_OBJECT = "java.lang.Double"
FLOAT_OBJECT = "java.lang.Float"
BYTE_OBJECT = "java.lang.Byte"
BOOLEAN_OBJECT = "java.lang.Boolean"
SHORT_OBJECT = "java.lang.Short"
CHAR_OBJECT = "java.lang.Char"
LONG_OBJECT = "java.lang.Long"
PRIMITIVES_WRAPPER = [ STRING_OBJECT, INTEGER_OBJECT, DOUBLE_OBJECT, FLOAT_OBJECT, BYTE_OBJECT, BOOLEAN_OBJECT, SHORT_OBJECT, CHAR_OBJECT ]
LONG = "J"
DOUBLE = "D"
FLOAT = "F"
INT = "I"
BYTE = "B"
SHORT = "S"
BOOLEAN = "Z"
CHAR = "C"
PRIMITIVES = [ "J", "D", "F", "I", "B", "S", "Z", "C" ]
NUMERICS = [ INT, CHAR, BOOLEAN, BYTE, SHORT, INTEGER_OBJECT, CHAR_OBJECT, BYTE_OBJECT, BOOLEAN_OBJECT, SHORT_OBJECT ]
ARRAYLIST = "java.util.ArrayList"
LINKEDLIST = "java.util.LinkedList"
VECTOR = "java.util.Vector"
ListTypes = [ ARRAYLIST, LINKEDLIST, VECTOR ]
prev_index = 0
INDENTATION = 15
# Indicates that obfuscated type names should be used in the RPC payload.
FLAG_ELIDE_TYPE_NAMES = 0x1
# Indicates that RPC token is included in the RPC payload.
FLAG_RPC_TOKEN_INCLUDED = 0x2
# Bit mask representing all valid flags.
VALID_FLAGS_MASK = 0x3
class GWTParser(object):
def _cleanup(self):
self.data = []
self.rpc_deserialized = []
self.rpc_list = []
self.indices = []
self.data_types = []
self.parameters = []
self.stream_version = 0
self.flags = 0
self.columns = 0
self.parameter_idx = 0
self.fuzzmarked = dict()
'''
Sets a value as fuzzable
'''
def _set_fuzzable( self, idx, fuzz_value ):
fuzz_idx = int(idx)+2
if self.surround_value:
if not fuzz_idx in self.fuzzmarked:
self.rpc_list_fuzzable[fuzz_idx] = self.surround_value+self.rpc_list_fuzzable[fuzz_idx]+self.surround_value
elif self.replace_value:
self.rpc_list_fuzzable[fuzz_idx] = self.replace_value
elif self.burp:
if not fuzz_idx in self.fuzzmarked:
self.rpc_list_fuzzable[fuzz_idx] = '§'+self.rpc_list_fuzzable[fuzz_idx]+'§'
else:
self.rpc_list_fuzzable[fuzz_idx] = fuzz_value
self.fuzzmarked[fuzz_idx] = fuzz_value
'''
Check if the next index value is an integer or a data index
'''
def _nextval_is_an_integer(self, curr_index):
if len(self.indices) == 0:
return False
# If the index is out of data array scope
if int(self.indices[0]) <= 4 or int(self.indices[0]) > self.columns:
return True
# If next index is the increment of the previous
if int(curr_index) == int(self.indices[0])-1:
if len(self.indices) > 1:
if int(self.indices[0]) == int(self.indices[1]):
return True
return False
'''
Put back the index that was pop previously
'''
def _indice_rollback(self, prev_index):
self.indices_read -= 1
self.indices.insert(0,prev_index)
'''
Is the next value an object type name
'''
def _is_an_object(self, value):
obj_check = re.compile( ".*/\d+" )
match = obj_check.match(value)
if match:
return True
return False
'''
Check to see if the type is an array of primitives
'''
def _is_a_primitive_array(self, data_type):
arr_check = re.compile( "\[(\w)/\d+" )
match = arr_check.match(data_type)
if match and match.group(1) in PRIMITIVES:
self.parameters[self.pidx].typename = match.group(1)
self.parameters[self.pidx].is_array = True
return True
return False
'''
Is the passed in value the subtype value for the current parameters typename
'''
def _is_object_subtype( self, data_type ):
typename = ""
if self.parameters[self.pidx].is_list:
typename = self.parameters[self.pidx].subtype
else:
typename = self.parameters[self.pidx].typename
subtype_check = re.compile( typename+"/\d+" )
match = subtype_check.match( data_type )
if match:
return True
return False
'''
Check to see if the type is array of objects
'''
def _is_an_object_array( self, data_type):
arr_check = re.compile( "\[L(.*);/\d+" )
match = arr_check.match(data_type)
if match:
self.parameters[self.pidx].typename = match.group(1)
self.parameters[self.pidx].is_array = True
return True
return False
'''
Check to see if data_type is a ListType
'''
def _is_list_type( self, data_type ):
if self._get_typename(data_type) in ListTypes:
return True
return False
'''
Check to see if the index passed in is an integer value
- This check needs some major work
'''
def _indice_is_intval( self, idx ):
# If the index is out of data array scope
if int(self.indices[int(idx)]) <= 4 or int(self.indices[int(idx)]) > self.columns:
return True
return False
'''
Check to see if the remaining method parameters are all numeric
'''
def _remaining_params_are_numeric( self, tracker_idx):
for i in range( self.pidx+1, len(self.parameters)):
if not self._get_typename(self.parameters[i].typename) in NUMERICS:
return False
return True
'''
Checks to see whether we should stop reading values into a custom object
'''
def _is_end_of_object( self, prev_index, value ):
tracker_idx = 0
found = False
if self._remaining_params_are_numeric( tracker_idx ):
if len(self.indices) == len(self.parameters[self.pidx+1:]):
prev_index = self.indices[0]
self._add_stringval(value)
return True
else:
return False
if len(self.parameters[self.pidx+1:]) == len(self.indices):
prev_index = self.indices[0]
self._add_stringval(value)
return True
for i in range( self.pidx+1, len( self.parameters ) ):
# Look Into the Future and see if the parameter values are still there
for j in range( tracker_idx, len(self.indices) ):
if self._get_typename(self.parameters[i].typename) in NUMERICS:
if self._indice_is_intval(j):
found = True
tracker_idx = j
continue
elif self._get_typename(self.parameters[i].typename) == STRING_OBJECT:
# If the index is out of data array scope
if int(self.indices[j]) <= 4 or int(self.indices[j]) > self.columns:
continue
if self._is_an_object( self.data[int(self.indices[j])] ) is False:
found = True
tracker_idx = j
break
else:
# If the index is out of data array scope
if int(self.indices[j]) <= 4 or int(self.indices[j]) > self.columns:
continue
# This must be a custom object. Check for the subtype..
if self._get_typename(self.data[int(self.indices[j])]) == self._get_typename(self.parameters[i].typename):
found = True
tracker_idx = j
break
if not found:
return True # Did not find the next parameter so the current value is the next method param
else:
found = False
return False
'''
Removes the "/" and digits from a typename
'''
def _get_typename( self, data_type):
subtype_check = re.compile( "(.*)/\d+" )
match = subtype_check.match(data_type)
if match:
return match.group(1)
return data_type
'''
Get the next index or integer value
'''
def _pop_index(self):
try:
self.indices_read += 1
index = int(self.indices.pop(0))
except TypeError:
print ("Invalid Integer given for indices")
sys.exit()
return index
'''
Get the next float value
'''
def _pop_float_index(self):
try:
self.indices_read += 1
index = float(self.indices.pop(0))
except TypeError:
print ("Invalid float value read")
sys.exit()
return index
'''
Pop the next index value and then return the corresponding value
from the data table
'''
def _get_nextval(self):
return self.data[self._pop_index()]
def _add_intval(self):
if self.parameters[self.pidx].flag:
self.parameters[self.pidx].values[self.aidx].values.append(self._pop_index())
elif self.parameters[self.pidx].is_list and self.parameters[self.pidx].is_custom_obj:
self.parameters[self.pidx].values[self.lidx].values.append(self._pop_index())
else:
self.parameters[self.pidx].values.append(self._pop_index())
def _add_stringval(self, value):
if self.parameters[self.pidx].flag:
self.parameters[self.pidx].values[self.aidx].values.append(value)
elif self.parameters[self.pidx].is_list and self.parameters[self.pidx].is_custom_obj:
self.parameters[self.pidx].values[self.lidx].values.append(value)
else:
self.parameters[self.pidx].values.append(value)
###################################
#
# Parsing Methods
#
####################################
def _parse_read_string(self):
self._set_fuzzable( self.indices[0], FUZZ_STRING )
if self.parameters[self.pidx].flag:
self.parameters[self.pidx].values[self.aidx].append(self._get_nextval())
else:
if self.parameters[self.pidx].is_list:
subtype = self._get_nextval()
self.parameters[self.pidx].values.append(self._get_nextval())
def _parse_read_int_byte_short_char(self, is_wrapper=False):
if is_wrapper:
subtype = self._get_nextval()
self._set_fuzzable( self.indices_read+self.columns, FUZZ_DIGIT )
if self.parameters[self.pidx].flag:
self.parameters[self.pidx].values[self.aidx].values.append(self._pop_index())
else:
self.parameters[self.pidx].values.append(self._pop_index())
def _parse_read_long(self, is_wrapper=False):
if is_wrapper:
subtype = self._get_nextval()
value1 = self._pop_float_index()
value2 = self._pop_float_index()
self._set_fuzzable( self.indices_read+self.columns-2, FUZZ_DIGIT )
self._set_fuzzable( self.indices_read+self.columns-1, FUZZ_DIGIT )
if value2 > 0:
self.parameters[self.pidx].values.append( str(value1) + str(value2) )
else:
self.parameters[self.pidx].values.append( str(value1) )
def _parse_read_double_float(self, is_wrapper=False):
if is_wrapper:
subtype = self._get_nextval()
self._set_fuzzable( self.indices_read+self.columns, FUZZ_DIGIT )
self.parameters[self.pidx].values.append(self._pop_float_index())
def _parse_primitive_array(self):
subtype = self._get_nextval()
how_many = self._pop_index()
for i in range(how_many):
self._parse_value( self.parameters[self.pidx].typename )
def _parse_object_array(self):
if self.parameters[self.pidx].flag is False:
subtype = self._get_nextval()
how_many = self._pop_index()
self.aidx = 0
for i in range(how_many):
self._parse_value(self.parameters[self.pidx].typename )
self.aidx += 1
def _parse_read_boolean(self):
self._set_fuzzable( self.indices_read+self.columns, FUZZ_DIGIT )
int_value = self._pop_index()
if int_value == 1:
self.parameters[self.pidx].values.append( "true" )
else:
self.parameters[self.pidx].values.append( "false" )
def _parse_read_list(self, list_type, set_list_flag=True):
if self.parameters[self.pidx].flag is False:
if set_list_flag:
self.parameters[self.pidx].is_list = True
self.parameters[self.pidx].typename = list_type
subtype = self._get_nextval()
else:
self.parameters[self.pidx].values[self.aidx].typename = list_type
how_many = self._pop_index()
self.lidx = 0
for i in range(how_many):
prev_index = self.indices[0]
if self.parameters[self.pidx].flag: # Reading a List within a Custom Object
subtype = self._get_typename(self._get_nextval())
self.parameters[self.pidx].values[self.aidx].subtype = subtype
self._indice_rollback(prev_index)
self._parse_value(subtype)
else: # Read values within a List Method Parameter
self.parameters[self.pidx].subtype = self._get_typename(self._get_nextval())
self._indice_rollback(prev_index)
self._parse_value(self.parameters[self.pidx].subtype)
self.lidx += 1
def _parse_read_object(self,name):
if len( self.indices ) > 0:
prev_index = self.indices[0]
self.parameters[self.pidx].is_custom_obj = True
value = self._get_nextval()
if self.parameters[self.pidx].is_array and self.parameters[self.pidx].is_custom_obj:
customParam = Parameter( value )
customParam.is_custom_obj = True
self.parameters[self.pidx].values.append( customParam )
self.parameters[self.pidx].flag = True
if self.parameters[self.pidx].is_list and self.parameters[self.pidx].is_custom_obj:
customParam = Parameter(value)
customParam.is_custom_obj = True
self.parameters[self.pidx].values.append(customParam)
# If this is the final parameter just read the remaining data as member variables
if len(self.parameters)-1 == self.pidx:
while len(self.indices) > 0: # Read till the end of the index table
if self._nextval_is_an_integer( prev_index ):
prev_index = self.indices[0]
self._set_fuzzable( self.indices_read+self.columns, FUZZ_DIGIT )
self._add_intval()
continue
else:
prev_index = self.indices[0]
value = self._get_nextval()
if self.parameters[self.pidx].is_array or self.parameters[self.pidx].is_list: # Am I reading an array of objects?
if self._is_object_subtype(value): # Did I just read in an object subtype?
self._indice_rollback(prev_index)
break # Stop reading object and move onto the next object in the array
if self._is_list_type( value ):
if self.parameters[self.pidx].flag is False:
self._indice_rollback(prev_index)
self._parse_read_list(self._get_typename(value), False)
elif self._is_an_object(value): # Is the value I just read in a subtype for another class
prev_index = self.indices[0]
else:
self._add_stringval(value)
self._set_fuzzable( prev_index, FUZZ_STRING )
else: # There are more parameters so we must be careful with the parsing
while len(self.indices) > 0:
if self._nextval_is_an_integer( prev_index ):
self._set_fuzzable( self.indices_read+self.columns, FUZZ_DIGIT )
prev_index = self.indices[0]
self._add_intval()
else:
prev_index = self.indices[0]
value = self._get_nextval()
if self.parameters[self.pidx].is_array or self.parameters[self.pidx].is_list:
if self._is_object_subtype(value):
self._indice_rollback(prev_index)
break;
if self._is_end_of_object( prev_index, value ):
if self.parameters[self.pidx].is_list or self.parameters[self.pidx].is_array:
self.pidx += 1
self._indice_rollback(prev_index)
self._parse_value( self.parameters[self.pidx].typename )
else:
if not self._get_typename(self.parameters[self.pidx+1].typename) in NUMERICS:
self._indice_rollback(prev_index)
break
elif self._is_an_object( value ):
continue
else: # store value
self._set_fuzzable( prev_index, FUZZ_STRING )
prev_index = self.indices[0]
self._add_stringval(value)
self.parameters[self.pidx].flag = False
'''
Split the object into a list and remove the last element
The RPC String ends with a '|' so this will create an empty element
'''
def _read_string_into_list(self):
# This copy is used to keep track of fuzzable values
self.rpc_list_fuzzable = list(self.rpc_string.split('|'))
self.rpc_list_fuzzable.pop()
# This copy is used to parsing and will have values removed during parsing
self.rpc_list = self.rpc_string.split('|')
self.rpc_list.pop()
'''
Store and remove the first three elements of the list
'''
def _get_headers(self):
try:
self.stream_version = int(self.rpc_list.pop(0))
self.flags = int(self.rpc_list.pop(0))
self.columns = int(self.rpc_list.pop(0))
except TypeError:
print ("Invalid Integer given for the stream_version or number of columns")
sys.exit()
'''
Store the data inside of the serialized object
I add in an empty string in the 0 Element in order to
stay uniform with the indices table in the RPC Object
'''
def _get_data(self):
self.data = self.rpc_list[0:self.columns]
self.data.insert(0,"")
'''
Store the indices that are found at the end of the RPC serialized object
'''
def _get_indices(self):
self.indices = self.rpc_list[self.columns:]
'''
Parses a value from the string table
'''
def _parse_value(self, data_type):
if self._get_typename(data_type) == STRING_OBJECT:
self._parse_read_string()
elif self._get_typename(data_type) == INT or data_type == BYTE or data_type == SHORT or data_type == CHAR:
self._parse_read_int_byte_short_char()
elif self._get_typename(data_type) == INTEGER_OBJECT or data_type == BYTE_OBJECT or data_type == SHORT_OBJECT or data_type == CHAR_OBJECT:
self._parse_read_int_byte_short_char(True)
elif self._get_typename(data_type) == LONG:
self._parse_read_long()
elif self._get_typename(data_type) == LONG_OBJECT:
self._parse_read_long(True)
elif self._get_typename(data_type) == DOUBLE or data_type == FLOAT:
self._parse_read_double_float()
elif self._get_typename(data_type) == DOUBLE_OBJECT or data_type == FLOAT_OBJECT:
self._parse_read_double_float(True)
elif self._is_a_primitive_array(data_type):
self._parse_primitive_array()
elif self._is_an_object_array(data_type):
self._parse_object_array()
elif self._get_typename(data_type) == BOOLEAN:
self._parse_read_boolean()
elif self._is_list_type(data_type):
self._parse_read_list(data_type)
else:
self._parse_read_object(data_type)
'''
Parses the GWT-RPC Request Payload
'''
def _parse(self):
self.rpc_deserialized = []
self.parameters = [] # Stores Parameter names and values read in from the request
self.pidx = 0 # Index value used to know which Parameter we are currently writing into
self.indices_read = 1 # Keeps track how many indices we have read
'''
Store the first four values
Hostname, Hash, Class Name, Method
'''
# rpc request has an rpc/xsrf token
if (self.flags & FLAG_RPC_TOKEN_INCLUDED):
self.rpc_deserialized.append(self._get_nextval()) # hostname
self.rpc_deserialized.append(self._get_nextval()) # strong name
self.rpc_token = {}
self.rpc_token[self._get_nextval()] = self._get_nextval()
self.rpc_deserialized.append(self._get_nextval()) # interface
self.rpc_deserialized.append(self._get_nextval()) # method
else:
for i in range(4):
self.rpc_deserialized.append(self._get_nextval())
for index in self.indices:
num_of_params = self._pop_index() # Number of Method parameters
for i in range(num_of_params):
self.parameters.append( Parameter(self._get_nextval()) )
for param in self.parameters:
if num_of_params > self.pidx: # If parameter index is greater than number of params then we are done
self._parse_value(param.typename)
self.pidx += 1
'''
Handles the parsing of the RPC string
'''
def deserialize(self, rpc_string):
self._cleanup()
self.rpc_string = rpc_string
self._read_string_into_list()
self._get_headers()
self._get_data()
self._get_indices()
try:
self._parse()
except IndexError:
print( "Encountered Error During Parsing" )
def get_fuzzstr(self):
fuzzstr = "|".join( self.rpc_list_fuzzable )+"|"
if self.fout:
self.fout.write( fuzzstr+"\n" )
else:
print( "\nGWT RPC Payload Fuzz String\n" )
print( fuzzstr+"\n" )
'''
Prints out the deserialized method call in a user friendly format
'''
def display(self):
if self.fout:
self.fout.write("==================================\n")
self.fout.write(str("Serialized Object:").rjust(INDENTATION) + "\n" + self.rpc_string + "\n\n")
self.fout.write(str("Stream Version:").rjust(INDENTATION) + "\t" + str(self.stream_version)+"\n")
self.fout.write(str("Flags:").rjust(INDENTATION) + "\t" + str(self.flags+"\n"))
self.fout.write(str("Column Numbers:").rjust(INDENTATION) + "\t" + str(self.columns)+"\n")
self.fout.write(str("Host:").rjust(INDENTATION) + "\t" + self.rpc_deserialized[0]+"\n")
self.fout.write(str("Hash:").rjust(INDENTATION) + "\t" + self.rpc_deserialized[1]+"\n")
self.fout.write(str("Class Name:").rjust(INDENTATION) + "\t" + self.rpc_deserialized[2]+"\n")
self.fout.write(str("Method:").rjust(INDENTATION) + "\t" + self.rpc_deserialized[3] + "\n")
self.fout.write(str("# of Params:").rjust(INDENTATION) + "\t" + str(len(self.parameters)) + "\n")
self.fout.write(str("Parameters:").rjust(INDENTATION)+"\n")
else:
print (str("\nSerialized Object:").rjust(INDENTATION) + "\n" + self.rpc_string + "\n")
print (str("Stream Version:").rjust(INDENTATION) + "\t" + str(self.stream_version))
print (str("Flags:").rjust(INDENTATION) + "\t" + str(self.flags))
print (str("Column Numbers:").rjust(INDENTATION) + "\t" + str(self.columns))
print (str("Host:").rjust(INDENTATION) + "\t" + self.rpc_deserialized[0])
print (str("Hash:").rjust(INDENTATION) + "\t" + self.rpc_deserialized[1])
print (str("Class Name:").rjust(INDENTATION) + "\t" + self.rpc_deserialized[2])
print (str("Method:").rjust(INDENTATION) + "\t" + self.rpc_deserialized[3])
print (str("# of Params:").rjust(INDENTATION) + "\t" + str(len(self.parameters)) + "\n")
print (str("Parameters:").rjust(INDENTATION))
for parameter in self.parameters:
if self.fout:
pprint.pprint(parameter.__dict__, stream=self.fout, indent="1")
else:
pprint.pprint(parameter.__dict__, indent="1")
print( "\n" )
if self.fout:
self.fout.write( "\n" )
else:
print ("\n")
def __init__( self ):
self.burp = False
self.surround_value = ""
self.replace_value = ""
self.fout = None
================================================
FILE: gwtparse/Parameter.py
================================================
#!/usr/bin/env python
class Parameter(object):
def __init__(self, tn ):
self.typename = tn
self.values = []
self.flag = False
self.is_custom_obj = False
self.is_list = False
self.is_array = False
def _add_value(self, val):
values.append( val )
def _set_flag(self, flag_value ):
self.flag = flag_value
def __repr__(self):
return "" % self.__dict__
================================================
FILE: gwtparse/README
================================================
GwtParse v0.2
For a detailed explaination of the tool see http://www.gdssecurity.com/l/b/2010/05/03/fuzzing-gwt-rpc-requests/
Any issues encountered when using the tool should be sent to rgutierrez@gdssecurity.com
Files Included:
gwtparse.py: Command Line interface to parse GWT RPC request and identify all fuzzable values
GWTParse.py: Modules containing all parsing logic which can be used within your own python code
Parameter.py: Simple Python object which is used by GWTParse to store RPC method parameters
Important Note:
When using the -b flag it is noted that the proper unicode symbol will only be properly display within
terminals which allow unicode characters ( Linux, Cygwin rxvt ). When using on windows the the output
can be written to a text file using the "-w" or "-a" options in order to properly view the output.
===============================
Command Line Usage Examples
===============================
Identify all fuzzable values in the GWT RPC Request Payload using the default output format
> python gwtparse.py -i ""
Surrounds fuzzable value with the "Section" symbol for use within Burp Intruder
> python gwtparse.py -i "" -b
Surrounds fuzzable value with the user defined value for use within custom fuzzer
> python gwtparse.py -i "" -s ""
Replaces fuzzable value with the user defined value for use within custom fuzzer
> python gwtparse.py -i "" -r ""
Parses RPC String and displays output in a pretty format in order to better understand the values passed
> python gwtparse.py -i "" -p
Writes output to file ( File must not already exist )
> python gwtparse.py -i "" -w "outfile.txt"
Appends output to file
> python gwtparse.py -i "" -a "outfile.txt"
================================
GWTParse Module Usage
================================
The GWTParser.py and Parameter.py files should be placed in a directory within your python path and imported into your python code
These must be performed in the following order.
Create Object
gwt = GWTParser()
Specify surround, replace or burp output format ( If none specified the default format is used )
gwt.surround_value =
or
gwt.replace_value =
or
gwt.burp = True
Specify file output handler ( If you want to write to output, default is standard output )
gwt.fout = < FILE OUTPUT HANDLER >
Deserialize the rpc request
gwt.deserialize( )
Display Pretty Output
gwt.display()
Return fuzz string
gwt.get_fuzzstr()
Data Access
Service Name: gwt.rpc_deserialized[2]
Method Name: gwt.rpc_deserialize[3]
Method Parameters: gwt.parameters
================================================
FILE: gwtparse/__init__.py
================================================
================================================
FILE: gwtparse/gwtparse.py
================================================
# -*- coding: utf-8 -*-
#!/usr/bin/env python
"""
GwtParse v0.2
Copyright (C) 2010 Ron Gutierrez
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
import os
from optparse import OptionParser
from GWTParser import GWTParser
desc = "A tool for parsing GWT RPC Requests"
if __name__ == "__main__":
parser = OptionParser(usage='usage: %prog [options]', description=desc, version='%prog 0.10')
parser.add_option('-p', '--pretty', help="Output the GWT RPC Request in a human readible format", action="store_true")
parser.add_option('-s', '--surround', help="String used to surround all fuzzable values", action="store", dest="surround_value")
parser.add_option('-r', '--replace', help="String used to replace all fuzzable values", action="store", dest="replace_value")
parser.add_option('-b', '--burp', help="Generates Burp Intruder Output", default=False, action="store_true")
parser.add_option('-i', '--input', help="RPC Request Payload (Required)", action="store", dest="rpc_request")
parser.add_option('-w', '--write', help="Writes Fuzz String to a new output file", action="store" )
parser.add_option('-a', '--append', help="Appends Fuzz String to an existing output file", action="store" )
(options, args) = parser.parse_args()
if options.rpc_request:
if options.surround_value and options.replace_value and options.burp:
print( "\nCannot choose more then one output format.\n" )
parser.print_help()
exit()
if options.surround_value and options.replace_value:
print( "\nCannot choose more then one output format.\n" )
parser.print_help()
exit()
if options.surround_value and options.burp:
print( "\nCannot choose more then one output format.\n" )
parser.print_help()
exit()
if options.replace_value and options.burp:
print( "\nCannot choose more then one output format.\n" )
parser.print_help()
exit()
gwt = GWTParser()
if options.surround_value:
gwt.surround_value = options.surround_value
elif options.replace_value:
gwt.replace_value = options.replace_value
elif options.burp:
gwt.burp = options.burp
if options.write:
if os.path.exists(options.write):
print( "Output file entered already exists" )
exit()
fout = open( options.write, "w" )
gwt.fout = fout
elif options.append:
fout = open( options.append, "a" )
gwt.fout = fout
gwt.deserialize( options.rpc_request )
if options.pretty:
gwt.display()
gwt.get_fuzzstr()
if gwt.fout:
gwt.fout.close()
else:
print( "\nMissing RPC Request Payload\n" )
parser.print_help()