├── README.md ├── LICENSE └── unsafe_jar_rs_scan.py /README.md: -------------------------------------------------------------------------------- 1 | Unsafe JAX-RS extension for Burp Suite 2 | ====================================== 3 | 4 | Unsafe JAX-RS is an active scanner extension for Burp Suite to check JAX-RS application for common security flaws. Currently following checks are implemented: 5 | * Entity provider selection scan 6 | * WADL scan 7 | * CSRF scan 8 | * JSONP scan 9 | * Async jobs scan 10 | * DoS via GZIP bombing scan 11 | * Content negotiation scan 12 | * Exception mapping scan 13 | 14 | Extension can identify following issues: 15 | * CVE-2016-6346 16 | * CVE-2016-8739 17 | * CVE-2016-7050 18 | * CVE-2016-6345 19 | * CVE-2016-9571 20 | * CVE-2016-6347 21 | * CVE-2016-3720 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Context Information Security 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /unsafe_jar_rs_scan.py: -------------------------------------------------------------------------------- 1 | try: 2 | from burp import IBurpExtender 3 | from burp import IScannerCheck 4 | from burp import IExtensionStateListener 5 | from burp import IHttpRequestResponse 6 | from burp import IScanIssue 7 | from burp import IParameter 8 | from burp import IScannerInsertionPointProvider 9 | from burp import IScannerInsertionPoint 10 | from array import array 11 | from random import * 12 | from string import * 13 | from re import * 14 | import StringIO 15 | import gzip 16 | from urlparse import urlparse 17 | except ImportError: 18 | print "Failed to load dependencies. This issue maybe caused by using an unstable Jython version." 19 | 20 | VERSION = '0.1' 21 | DEBUG = False # Turn on/off debug info in console 22 | callbacks = None 23 | helpers = None 24 | 25 | def debug2console(title, *args): 26 | if DEBUG: 27 | print "[ debug ]", "Begin", title 28 | for arg in args: 29 | print arg 30 | print "[ debug ]", "End", title 31 | 32 | def gzip_encode(str): 33 | out = StringIO.StringIO() 34 | with gzip.GzipFile(fileobj=out, mode="w") as f: 35 | f.write(str) 36 | return out.getvalue() 37 | 38 | def safe_bytes_to_string(bytes): 39 | if bytes is None: 40 | bytes = '' 41 | 42 | return helpers.bytesToString(bytes) 43 | 44 | def should_trigger_per_request_attacks(request_info, insertionPoint): ### Hack to make per-request scan from @albinowax 45 | params = request_info.getParameters() 46 | 47 | if params: 48 | first_parameter_offset = 999999 49 | first_parameter = None 50 | for param_type in (IParameter.PARAM_BODY, IParameter.PARAM_URL, IParameter.PARAM_JSON, IParameter.PARAM_XML, 51 | IParameter.PARAM_XML_ATTR, IParameter.PARAM_MULTIPART_ATTR, IParameter.PARAM_COOKIE): 52 | for param in params: 53 | if param.getType() != param_type: 54 | continue 55 | if param.getNameStart() < first_parameter_offset: 56 | first_parameter_offset = param.getNameStart() 57 | first_parameter = param 58 | if first_parameter: 59 | break 60 | 61 | if first_parameter and first_parameter.getName() == insertionPoint.getInsertionPointName(): 62 | return True 63 | 64 | elif insertionPoint.getInsertionPointType() == IScannerInsertionPoint.INS_HEADER and \ 65 | insertionPoint.getInsertionPointName() == 'User-Agent': 66 | 67 | return True 68 | 69 | debug2console('Skiping insertion point', insertionPoint.getInsertionPointName()) 70 | return False 71 | 72 | def get_random_string(len): 73 | return "".join([choice(ascii_letters) for _ in range(len)]) 74 | 75 | def get_request_headers_as_dict(request): 76 | rawHeaders = helpers.analyzeRequest(request).getHeaders() 77 | return dict((header.split(':')[0], header.split(':', 1)[1].strip()) for header in rawHeaders[1:]) 78 | 79 | 80 | def get_response_headers_as_dict(response): 81 | rawHeaders = helpers.analyzeResponse(response).getHeaders() 82 | return dict((header.split(':')[0], header.split(':', 1)[1].strip()) for header in rawHeaders[1:]) 83 | 84 | def get_response_status_code(response): 85 | return helpers.analyzeResponse(response).getStatusCode(); 86 | 87 | def get_response_body(response): 88 | offset = helpers.analyzeResponse(response).getBodyOffset() 89 | return response[offset:] 90 | 91 | def get_request_body(request): 92 | offset = helpers.analyzeRequest(request).getBodyOffset() 93 | return request[offset:] 94 | 95 | # Add header or modify existing 96 | def add_header_to_request(request, header_name, header_value): 97 | info = helpers.analyzeRequest(request) 98 | 99 | requestBodyOffset = info.getBodyOffset() 100 | requestHeaders = request[:requestBodyOffset].split('\r\n') 101 | requestBody = request[requestBodyOffset:] 102 | 103 | headerExists = len(filter( lambda x: header_name in x, requestHeaders )) > 0 104 | 105 | modifiedHeaders = "" 106 | 107 | if headerExists: 108 | modifiedHeaders = "\r\n".join([header if header_name not in header else header_name + header_value for header in requestHeaders]) 109 | else: 110 | modifiedHeaders = "\r\n".join([header if "Host: " not in header else header + "\r\n" + header_name + header_value for header in requestHeaders]) 111 | 112 | return modifiedHeaders + requestBody 113 | 114 | def remove_header_from_request(request, header_name): 115 | info = helpers.analyzeRequest(request) 116 | 117 | requestBodyOffset = info.getBodyOffset() 118 | requestHeaders = request[:requestBodyOffset].split('\r\n') 119 | requestBody = request[requestBodyOffset:] 120 | 121 | headerExists = len(filter( lambda x: header_name in x, requestHeaders )) > 0 122 | 123 | if headerExists: 124 | modifiedHeaders = "\r\n".join([header for header in requestHeaders if header_name not in header]) 125 | return modifiedHeaders + requestBody 126 | 127 | return request 128 | 129 | def add_body_to_request(request, body): 130 | info = helpers.analyzeRequest(request) 131 | requestBodyOffset = info.getBodyOffset() 132 | requestHeaders = request[:requestBodyOffset] 133 | requestBody = request[requestBodyOffset:] 134 | 135 | h = helpers.stringToBytes(requestHeaders) 136 | b = helpers.stringToBytes(body) 137 | h.extend(b) 138 | 139 | return add_header_to_request(safe_bytes_to_string(h),"Content-Length: ",str(len(body))) 140 | 141 | def prepare(basePair): 142 | request = safe_bytes_to_string(basePair.getRequest()) 143 | response = basePair.getResponse() 144 | request_headers = get_request_headers_as_dict(request) 145 | response_headers = get_response_headers_as_dict(response) 146 | info_request = helpers.analyzeRequest(request) 147 | info_response = helpers.analyzeResponse(response) 148 | 149 | return (request, response, request_headers, response_headers, info_request, info_response) 150 | 151 | def hyperlink(text, href): 152 | return "{}".format(href, text) 153 | 154 | def isOk(status): 155 | return status in (200, 201, 202, 204) 156 | 157 | 158 | class BurpExtender(IBurpExtender): 159 | 160 | def registerExtenderCallbacks(self, this_callbacks): 161 | global callbacks, helpers 162 | callbacks = this_callbacks 163 | helpers = callbacks.getHelpers() 164 | callbacks.setExtensionName("Unsafe JAX-RS") 165 | 166 | callbacks.registerScannerCheck(JaxRsScanner()) 167 | callbacks.registerScannerCheck(JerseyScanner()) 168 | callbacks.registerScannerCheck(CXFJaxRsScanner()) 169 | callbacks.registerScannerCheck(ResteasyScanner()) 170 | 171 | print "Successfully loaded Unsafe JAX-RS v" + VERSION 172 | 173 | return 174 | 175 | 176 | class ScanIssue(IScanIssue): 177 | 178 | def __init__(self, httpService, url, httpMessages, name, detail, confidence, severity): 179 | self.HttpService = httpService 180 | self.Url = url 181 | self.HttpMessages = httpMessages 182 | self.Name = name 183 | self.Detail = detail 184 | self.Severity = severity 185 | self.Confidence = confidence 186 | print "Reported: " + name + " on " + str(url) 187 | return 188 | 189 | def getUrl(self): 190 | return self.Url 191 | 192 | def getIssueName(self): 193 | return self.Name 194 | 195 | def getIssueType(self): 196 | return 0 197 | 198 | def getSeverity(self): 199 | return self.Severity 200 | 201 | def getConfidence(self): 202 | return self.Confidence 203 | 204 | def getIssueBackground(self): 205 | return None 206 | 207 | def getRemediationBackground(self): 208 | return None 209 | 210 | def getIssueDetail(self): 211 | return self.Detail 212 | 213 | def getRemediationDetail(self): 214 | return None 215 | 216 | def getHttpMessages(self): 217 | return self.HttpMessages 218 | 219 | def getHttpService(self): 220 | return self.HttpService 221 | 222 | 223 | class JaxRsScanner(IScannerCheck): 224 | 225 | DESCR_WADL_SCAN = "JAX-RS application exposes {0}. You should check manually or using {1} that all resource methods " \ 226 | "have proper authentication/authorization.".format( 227 | hyperlink("WADL","https://en.wikipedia.org/wiki/Web_Application_Description_Language"), 228 | hyperlink("SOAPUI","https://www.soapui.org/downloads/soapui.html")) 229 | 230 | DESCR_CONF_SCAN = "It seems that resource method of JAX-RS application lacks {0} annotation or have permissive media type specification " \ 231 | "e.g. \"application/*\". This can lead to situation when attacker can select \"bad\" {1} instead of intended provider. " \ 232 | "See {2}, {3}, {4} for more information about entity provider selection confusion vulnerabilities.".format( 233 | hyperlink("@Consumes","https://docs.oracle.com/cd/E19776-01/820-4867/ggqqr/"), 234 | hyperlink("Entity Provider","https://jersey.java.net/documentation/latest/message-body-workers.html"), 235 | hyperlink("CVE-2016-7050","https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2016-7050"), 236 | hyperlink("CVE-2016-9571","https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2016-9571"), 237 | hyperlink("CVE-2016-8739","https://bugzilla.redhat.com/show_bug.cgi?id=1406811")) 238 | 239 | DESCR_CSRF_SCAN = "It seems that resource method lacks {0} or allows text/plain media type. If class of entity parameter " \ 240 | "has valueOf(String) method or String constructor, this JAX-RS method might be vulnerable to CSRF attack.".format( 241 | hyperlink("@Consumes","https://docs.oracle.com/cd/E19776-01/820-4867/ggqqr/")) 242 | 243 | DESCR_DOS_GZIP_SCAN = "JAX-RS resource method is vulnerable to DoS attack via decompression bomb. See {0}. " \ 244 | "Futher reading about decompression bombs - {1}.".format( 245 | hyperlink("CVE-2016-6346","https://access.redhat.com/security/cve/cve-2016-6346"), 246 | hyperlink("I Came to Drop Bombs ","https://www.blackhat.com/docs/us-16/materials/us-16-Marie-I-Came-to-Drop-Bombs-Auditing-The-Compression-Algorithm-Weapons-Cache.pdf")) 247 | 248 | DESCR_JSONP_SCAN = "JAX-RS resource method supports {0}. It might be vulnerable to {1} attack.".format( 249 | hyperlink("JSONP","https://en.wikipedia.org/wiki/JSONP"), 250 | hyperlink("XSSI","https://www.scip.ch/en/?labs.20160414")) 251 | 252 | DESCR_EXC_MAP_SCAN = "JAX-RS application uses exception mappers which exposes stacktrace and allows to identify JAX-RS library." 253 | 254 | DESCR_EXC_MAP_SCAN_XSS = "JAX-RS application uses exception mapper for JSON unmarshalling which is vulnerable to XSS attack. " \ 255 | "XSS vulnerability in RESTEasy - CVE-2016-6347".format( 256 | hyperlink("CVE-2016-6347","https://access.redhat.com/security/cve/cve-2016-6347")) 257 | 258 | DESCR_URI_CT_NEG = "JAX-RS resource method supports negotiation of response media type via URI extension, e.g. /something.json." \ 259 | "It might lead to XSSI or XSS attacks." 260 | 261 | DESCR_XXE_SCAN = "It seems that resource method lacks {0} or specifies it too permisively. Additionally JAX-RS application has entity provider that is vulnerable to XXE. " \ 262 | "For example, JacksonJaxbXMLProvider which is the part of Jackson is known to be vulnerable to {1}.".format( 263 | hyperlink("@Consumes", "https://docs.oracle.com/cd/E19776-01/820-4867/ggqqr/"), 264 | hyperlink("CVE-2016-3720", "https://bugzilla.redhat.com/show_bug.cgi?id=1328427")) 265 | 266 | 267 | def doPassiveScan(self, basePair): 268 | return [] 269 | 270 | 271 | def doActiveScan(self, basePair, insertionPoint): 272 | issues = [] 273 | 274 | self.request, self.response, self.request_headers, self.response_headers, self.info_request, self.info_response = prepare(basePair) 275 | self.httpService = basePair.getHttpService() 276 | self.URL = helpers.analyzeRequest(basePair).getUrl() 277 | 278 | if not should_trigger_per_request_attacks(self.info_request, insertionPoint): 279 | return [] 280 | 281 | issues.extend(self.wadl_scan()) 282 | issues.extend(self.confusion_scan()) 283 | issues.extend(self.gzip_dos_scan()) 284 | issues.extend(self.jsonp_scan()) 285 | issues.extend(self.exception_mapper_scan()) 286 | issues.extend(self.csrf_scan()) 287 | issues.extend(self.uri_based_negotiation_scan()) 288 | issues.extend(self.xxe_scan()) 289 | 290 | return issues 291 | 292 | 293 | def xxe_scan(self): 294 | content_types = [ 295 | "application/xml", 296 | "text/xml" 297 | ] 298 | 299 | if not self.request_headers.has_key('Content-Length') or \ 300 | int(self.request_headers.get('Content-Length', 0)) == 0: 301 | return [] 302 | 303 | for ct in content_types: 304 | request = safe_bytes_to_string(add_header_to_request(self.request, "Content-Type: ", ct)) 305 | 306 | body = '' + \ 307 | '' + \ 308 | '' 309 | r = get_random_string(10) 310 | body = body % r 311 | 312 | request = safe_bytes_to_string(add_body_to_request(request, body)) 313 | 314 | newPair = callbacks.makeHttpRequest(self.httpService, request) 315 | response_modif = safe_bytes_to_string(newPair.getResponse()) 316 | 317 | debug2console("XXE Scan", request, response_modif) 318 | 319 | if get_response_status_code(response_modif) in (400, 500) and r in response_modif: 320 | return [ 321 | ScanIssue(self.httpService, self.URL, [newPair], 322 | "JAX-RS application has entity provider which is vulnerable to XXE", 323 | CXFJaxRsScanner.DESCR_XXE_SCAN, 324 | 'Certain', 'High'), 325 | ] 326 | 327 | return [] 328 | 329 | 330 | def csrf_scan(self): 331 | if not isOk(self.info_response.getStatusCode()): 332 | return [] 333 | 334 | if self.info_request.getMethod() not in ('GET','POST'): 335 | return [] 336 | 337 | if not self.request_headers.has_key('Content-Length') or \ 338 | int(self.request_headers.get('Content-Length',0)) == 0: 339 | return [] 340 | 341 | request = safe_bytes_to_string( add_header_to_request(self.request, "Content-Type: ", "text/plain") ) 342 | 343 | newPair = callbacks.makeHttpRequest(self.httpService, request) 344 | response_modif = safe_bytes_to_string( newPair.getResponse() ) 345 | 346 | debug2console("CSRF scan valueOf", request, response_modif) 347 | 348 | if get_response_status_code(response_modif) != 415: 349 | return [ 350 | ScanIssue(self.httpService, self.URL, [ newPair ], 351 | 'JAX-RS resource method is vulnerable to CSRF', 352 | JaxRsScanner.DESCR_CSRF_SCAN, 353 | 'Firm', 'Medium'), 354 | ] 355 | return [] 356 | 357 | 358 | def wadl_scan(self): 359 | """ Possible wadl servlet path values """ 360 | names = [ 361 | 'application.xml', 362 | 'application.wadl' 363 | ] 364 | 365 | path = self.info_request.getHeaders()[0].split(' ')[1] 366 | path_list = path.split('/') 367 | 368 | request = self.request 369 | 370 | if self.info_request.getMethod() != 'GET': 371 | request = safe_bytes_to_string( helpers.toggleRequestMethod(request) ) 372 | 373 | for i in range(1,len(path_list)) : 374 | for name in names: 375 | _path = "/".join(path_list[:i]) + "/" + name 376 | _request = request.replace(path, _path, 1) 377 | 378 | modifPair = callbacks.makeHttpRequest(self.httpService, _request) 379 | response_modif = safe_bytes_to_string(modifPair.getResponse()) 380 | 381 | debug2console("WADL Scan generic",_request,response_modif) 382 | 383 | _ct = get_response_headers_as_dict(response_modif).get('Content-Type') 384 | 385 | if isOk(get_response_status_code(response_modif)) and \ 386 | _ct in ("application/xml", "application/vnd.sun.wadl+xml"): 387 | 388 | return [ 389 | ScanIssue(self.httpService, self.URL, [ modifPair ], 390 | "JAX-RS application exposes WADL", 391 | JaxRsScanner.DESCR_WADL_SCAN, 392 | 'Certain', 'Medium'), 393 | ] 394 | return [] 395 | 396 | 397 | def confusion_scan(self): 398 | """ Interesting Content-Types to check """ 399 | confusion_cts = ( 400 | "", # empty 401 | "application/xml", 402 | "text/xml", 403 | "application/atom+xml", 404 | "application/x-yaml", 405 | "text/yaml", 406 | "application/x-kryo", 407 | "application/x-stream", 408 | "application/x-java-serialized-object", 409 | "text/plain;charset=" + get_random_string(10) 410 | ) 411 | 412 | if not isOk(self.info_response.getStatusCode()): 413 | return [] 414 | 415 | if not self.request_headers.has_key('Content-Length') or \ 416 | int(self.request_headers.get('Content-Length',0)) == 0: 417 | return [] 418 | 419 | issues = [] 420 | for ct in confusion_cts: 421 | _request = safe_bytes_to_string( add_header_to_request(self.request, "Content-Type: ", ct) ) 422 | 423 | body = get_random_string(10) 424 | 425 | _request = safe_bytes_to_string( add_body_to_request(_request, body) ) 426 | 427 | newPair = callbacks.makeHttpRequest(self.httpService, _request) 428 | response_modif = safe_bytes_to_string(newPair.getResponse()) 429 | 430 | debug2console("Confusion Scan", ct, _request, response_modif) 431 | 432 | if get_response_status_code(response_modif) in (500, 400): 433 | issues.append(ScanIssue(self.httpService, self.URL, [ newPair ], 434 | "JAX-RS application is vulnerable to entity provider selection confusion", 435 | JaxRsScanner.DESCR_CONF_SCAN, 436 | "Firm", "High")) 437 | return issues 438 | 439 | 440 | def gzip_dos_scan(self): 441 | if not isOk(self.info_response.getStatusCode()): 442 | return [] 443 | 444 | if not self.request_headers.has_key('Content-Length') or \ 445 | int(self.request_headers.get('Content-Length',0)) == 0: 446 | return [] 447 | 448 | body = gzip_encode(get_request_body( self.request )) 449 | 450 | _request = safe_bytes_to_string(add_header_to_request(self.request, 'Content-Encoding: ', 'gzip')) 451 | _request = safe_bytes_to_string( add_body_to_request(_request, body) ) 452 | 453 | modifPair = callbacks.makeHttpRequest(self.httpService, _request ) 454 | response_modif = safe_bytes_to_string(modifPair.getResponse()) 455 | 456 | debug2console("GZIP DoS Scan", response_modif) 457 | 458 | if isOk(get_response_status_code(response_modif)) and \ 459 | self.response_headers.get('Content-Type') == get_response_headers_as_dict(response_modif).get('Content-Type'): 460 | 461 | return [ 462 | ScanIssue(self.httpService, self.URL, [ modifPair, ], 463 | "JAX-RS resource is vulnerable to GZIP bombing DoS", 464 | JaxRsScanner.DESCR_DOS_GZIP_SCAN, 465 | "Certain", "Medium"), 466 | ] 467 | return [] 468 | 469 | 470 | def jsonp_scan(self): 471 | """ Possible parameter names for JSONP """ 472 | jsnop_param_names = [ 473 | "callback", 474 | "_callback", 475 | "__callback", 476 | "jsonp", 477 | "_jsonp", 478 | "__jsonp", 479 | "func", 480 | "function" 481 | ] 482 | 483 | if not isOk(self.info_response.getStatusCode()): 484 | return [] 485 | 486 | value = get_random_string(10) 487 | 488 | for param_name in jsnop_param_names: 489 | param = helpers.buildParameter(param_name , value, IParameter.PARAM_URL) 490 | _request = safe_bytes_to_string( helpers.addParameter(self.request,param) ) 491 | 492 | newPair = callbacks.makeHttpRequest(self.httpService, _request) 493 | response_modif = safe_bytes_to_string( newPair.getResponse() ) 494 | 495 | debug2console("JSONP Scan", _request, response_modif) 496 | 497 | if value + "(" in response_modif: 498 | return [ 499 | ScanIssue(self.httpService, self.URL, [ newPair ], 500 | "JAX-RS resource method supports JSONP", 501 | JaxRsScanner.DESCR_JSONP_SCAN, 502 | 'Firm', 'Medium'), 503 | ] 504 | return [] 505 | 506 | 507 | def exception_mapper_scan(self): 508 | issues = [] 509 | 510 | if not isOk(self.info_response.getStatusCode()): 511 | return [] 512 | 513 | path = self.info_request.getHeaders()[0].split(' ')[1] 514 | path_list = path.split('/') 515 | 516 | for i in range(1,len(path_list)): ### Check for PathParam processing exceptions 517 | _path_list = path_list[:] 518 | _path_list[i] = choice("{}[]\\") 519 | 520 | _path = "/".join(_path_list) 521 | _request = self.request.replace(path, _path, 1) 522 | 523 | modifPair = callbacks.makeHttpRequest(self.httpService, _request ) 524 | response_modif = safe_bytes_to_string(modifPair.getResponse()) 525 | 526 | debug2console("PathParam processing exception", _request, response_modif) 527 | 528 | if get_response_status_code(response_modif) == 500: 529 | issues.append( 530 | ScanIssue(self.httpService, self.URL, [ modifPair ], 531 | "Exception occured during Path Param processing", 532 | JaxRsScanner.DESCR_EXC_MAP_SCAN, 533 | 'Certain', 'Low') 534 | ) 535 | break 536 | 537 | ACCEPT = get_random_string(10) + "/" + get_random_string(10) ### Check for exceptions during marshalling 538 | _request = add_header_to_request(self.request, "Accept: ", ACCEPT) 539 | 540 | modifPair = callbacks.makeHttpRequest(self.httpService, _request ) 541 | response_modif = safe_bytes_to_string(modifPair.getResponse()) 542 | 543 | debug2console("Marshalling exceptions", _request, response_modif) 544 | 545 | if get_response_status_code(response_modif) == 500: 546 | issues.append( 547 | ScanIssue(self.httpService, self.URL, [ modifPair ], 548 | "Exception occured during marshalling", 549 | JaxRsScanner.DESCR_EXC_MAP_SCAN, 550 | 'Certain', 'Low') 551 | ) 552 | 553 | if not self.request_headers.has_key('Content-Length') or \ 554 | int(self.request_headers.get('Content-Length',0)) == 0: 555 | return issues 556 | 557 | if 'application/json' in self.request_headers['Content-Type']: ### Check for exceptions during JSON unmarshalling 558 | value = get_random_string(10) 559 | body = '{"<%s>":1}' % value 560 | _request = safe_bytes_to_string(add_body_to_request(self.request, body)) 561 | 562 | modifPair = callbacks.makeHttpRequest(self.httpService, _request ) 563 | response_modif = safe_bytes_to_string(modifPair.getResponse()) 564 | 565 | debug2console("Unmarshalling exceptions", _request, response_modif) 566 | 567 | if "<%s>" % value in response_modif: 568 | if "text/html" in get_response_headers_as_dict(response_modif).get('Content-Type',''): 569 | issues.append( 570 | ScanIssue(self.httpService, self.URL, [ modifPair ], 571 | "JAX-RS exception mapper is vulnerable to XSS", 572 | JaxRsScanner.DESCR_EXC_MAP_SCAN_XSS, 573 | 'Certain', 'Medium') 574 | ) 575 | else: 576 | issues.append( 577 | ScanIssue(self.httpService, self.URL, [ modifPair ], 578 | "Exception occured during JSON unmarshalling", 579 | JaxRsScanner.DESCR_EXC_MAP_SCAN, 580 | 'Certain', 'Low') 581 | ) 582 | return issues 583 | 584 | 585 | def uri_based_negotiation_scan(self): 586 | """ Content-Types to extension mapping """ 587 | mappings = { 588 | "application/json" : ".json", 589 | "application/xml" : ".xml", 590 | "text/xml": ".xml", 591 | "application/atom+xml": ".atom", 592 | "text/plain": ".txt", 593 | "text/html": ".html", 594 | "application/x-javascript": ".js" 595 | } 596 | 597 | if not isOk(self.info_response.getStatusCode()): 598 | return [] 599 | 600 | ct = self.response_headers.get('Content-Type','') 601 | ext = mappings.get(ct, ".json") 602 | 603 | ACCEPT = get_random_string(10) + "/" + get_random_string(10) 604 | request = add_header_to_request(self.request, "Accept: ", ACCEPT) 605 | 606 | path = urlparse(str(self.URL)).path 607 | 608 | request = request.replace(path, path + ext, 1) 609 | 610 | modifPair = callbacks.makeHttpRequest(self.httpService, request) 611 | response_modif = safe_bytes_to_string( modifPair.getResponse() ) 612 | 613 | debug2console("URI-based negotiation scan", request, response_modif) 614 | 615 | if isOk(get_response_status_code(response_modif)) and \ 616 | self.response_headers.get('Content-Type') == get_response_headers_as_dict(response_modif).get('Content-Type'): 617 | 618 | return [ 619 | ScanIssue(self.httpService, self.URL, [ modifPair ], 620 | "JAX-RS resource method supports URI-based content negotiation", 621 | JaxRsScanner.DESCR_URI_CT_NEG, 622 | "Firm", "Medium"), 623 | ] 624 | return [] 625 | 626 | 627 | class JerseyScanner(IScannerCheck): 628 | 629 | DESCR_WADL_SCAN = "Jersey application exposes {0}. You should check manually or using {1} that all resource methods " \ 630 | "have proper authentication/authorization.".format( 631 | hyperlink("WADL","https://en.wikipedia.org/wiki/Web_Application_Description_Language"), 632 | hyperlink("SOAPUI","https://www.soapui.org/downloads/soapui.html")) 633 | 634 | def doPassiveScan(self, basePair): 635 | return [] 636 | 637 | 638 | def doActiveScan(self, basePair, insertionPoint): 639 | issues = [] 640 | 641 | self.request, self.response, self.request_headers, self.response_headers, self.info_request, self.info_response = prepare(basePair) 642 | self.httpService = basePair.getHttpService() 643 | self.URL = helpers.analyzeRequest(basePair).getUrl() 644 | 645 | if not should_trigger_per_request_attacks(self.info_request, insertionPoint): 646 | return [] 647 | 648 | issues.extend(self.wadl_scan()) 649 | 650 | return issues 651 | 652 | 653 | def wadl_scan(self): 654 | request = self.request 655 | 656 | if self.info_request.getMethod() != 'GET': 657 | request = safe_bytes_to_string(helpers.toggleRequestMethod(request)) 658 | 659 | request = safe_bytes_to_string(add_header_to_request(request, "Accept: ", "application/vnd.sun.wadl+xml")) 660 | 661 | request_options = request.replace("GET ", "OPTIONS ", 1) 662 | 663 | newPair = callbacks.makeHttpRequest(self.httpService, request_options) 664 | resp = safe_bytes_to_string(newPair.getResponse()) 665 | 666 | debug2console("WADL Scan Jersey", request, resp) 667 | 668 | if not (isOk(get_response_status_code(resp)) and \ 669 | get_response_headers_as_dict(resp).get('Content-Type') == 'application/vnd.sun.wadl+xml'): 670 | return [] 671 | 672 | m = search('(?im)' + \ 894 | '' 895 | r = get_random_string(10) 896 | body = body % r 897 | 898 | request = safe_bytes_to_string( add_body_to_request(request, body) ) 899 | 900 | newPair = callbacks.makeHttpRequest(self.httpService, request) 901 | response_modif = safe_bytes_to_string(newPair.getResponse()) 902 | 903 | debug2console("CVE-2016-8739 Scan", request, response_modif) 904 | 905 | if get_response_status_code(response_modif) in (400, 500) and r in response_modif: 906 | return [ 907 | ScanIssue(self.httpService, self.URL, [ newPair ], 908 | "Apache CXF RS resource method is vulnerable to CVE-2016-8739", 909 | CXFJaxRsScanner.DESCR_CVE_2016_8739_SCAN, 910 | 'Certain', 'High'), 911 | ] 912 | return [] 913 | 914 | 915 | class ResteasyScanner(IScannerCheck): 916 | 917 | DESCR_CVE_2016_7050_SCAN = "Resource method of RESTEasy application lacks {0} annotation or have permissive media type specification. " \ 918 | "RESTEasy is vulnerable to Java deserialization attack - {1}. Attacker can trigger unmarshalling using vulnerable Serializable provider " \ 919 | "by specifying application/x-java-serialized-object Content-Type header.".format( 920 | hyperlink("@Consumes","https://docs.oracle.com/cd/E19776-01/820-4867/ggqqr/"), 921 | hyperlink("CVE-2016-7050","https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2016-7050")) 922 | 923 | DESCR_ASYNCH_SCAN = "RESTEasy application supports {0}. It might be vulnerable to {1}.".format( 924 | hyperlink("Async jobs","http://docs.jboss.org/resteasy/docs/3.1.0.Final/userguide/html_single/index.html#async_job_service"), 925 | hyperlink("CVE-2016-6345","https://access.redhat.com/security/cve/cve-2016-6345")) 926 | 927 | DESCR_CVE_2016_9571_SCAN = "Resource method of RESTEasy application lacks {0} annotation or have permissive media type specification. " \ 928 | "RESTEasy is vulnerable to Yaml unmarshalling attack - {1}. Attacker can trigger unmarshalling using vulnerable Yaml provider " \ 929 | "by specifying text/yaml Content-Type header.".format( 930 | hyperlink("@Consumes","https://docs.oracle.com/cd/E19776-01/820-4867/ggqqr/"), 931 | hyperlink("CVE-2016-9571","https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2016-9571")) 932 | 933 | DECSR_URL_PARAM_CT_MAPPING_SCAN = "RESTEasy application supports response content type negotiation via {0}. " \ 934 | "It might be vulnerable to XSSI or XSS attacks.".format( 935 | hyperlink("query string parameter","http://docs.jboss.org/resteasy/docs/3.1.0.Final/userguide/html_single/index.html#param_media_mappings")) 936 | 937 | 938 | def doActiveScan(self, basePair, insertionPoint): 939 | issues = [] 940 | 941 | self.request, self.response, self.request_headers, self.response_headers, self.info_request, self.info_response = prepare(basePair) 942 | self.httpService = basePair.getHttpService() 943 | self.URL = helpers.analyzeRequest(basePair).getUrl() 944 | 945 | if not should_trigger_per_request_attacks(self.info_request, insertionPoint): 946 | return [] 947 | 948 | issues.extend(self.cve_2016_7050_scan()) 949 | issues.extend(self.cve_2016_9571_scan()) 950 | issues.extend(self.asynch_scan()) 951 | issues.extend(self.url_mapping_scan()) 952 | 953 | return issues 954 | 955 | 956 | def doPassiveScan(self, basePair): 957 | return [] 958 | 959 | 960 | def cve_2016_7050_scan(self): 961 | if not self.request_headers.has_key('Content-Length') or \ 962 | int(self.request_headers.get('Content-Length',0)) == 0: 963 | return [] 964 | 965 | request = safe_bytes_to_string( add_header_to_request(self.request, "Content-Type: ", "application/x-java-serialized-object") ) 966 | 967 | body = get_random_string(4) 968 | 969 | request = safe_bytes_to_string( add_body_to_request(request, body) ) 970 | 971 | newPair = callbacks.makeHttpRequest(self.httpService, request) 972 | response_modif = safe_bytes_to_string( newPair.getResponse() ) 973 | 974 | debug2console("CVE-2016-7050 Scan", request, response_modif) 975 | 976 | if get_response_status_code(response_modif) in (400, 500) and "java.io.StreamCorruptedException" in response_modif: 977 | return [ 978 | ScanIssue(self.httpService, self.URL, [ newPair ], 979 | "RESTEasy resource method is vulnerable to CVE-2016-7050", 980 | ResteasyScanner.DESCR_CVE_2016_7050_SCAN, 981 | 'Certain', 'High'), 982 | ] 983 | return [] 984 | 985 | 986 | def cve_2016_9571_scan(self): 987 | if not self.request_headers.has_key('Content-Length') or \ 988 | int(self.request_headers.get('Content-Length',0)) == 0: 989 | return [] 990 | 991 | request = safe_bytes_to_string( add_header_to_request(self.request, "Content-Type: ", "text/yaml") ) 992 | 993 | body = get_random_string(10) 994 | 995 | request = safe_bytes_to_string( add_body_to_request(request, body) ) 996 | 997 | newPair = callbacks.makeHttpRequest(self.httpService, request) 998 | response_modif = safe_bytes_to_string( newPair.getResponse() ) 999 | 1000 | debug2console("CVE-2016-9571 Scan", request, response_modif) 1001 | 1002 | if get_response_status_code(response_modif) in (400, 500) and "java.lang.String " + body in response_modif: 1003 | return [ 1004 | ScanIssue(self.httpService, self.URL, [ newPair ], 1005 | "RESTEasy resource method is vulnerable to CVE-2016-9571", 1006 | ResteasyScanner.DESCR_CVE_2016_9571_SCAN, 1007 | 'Certain', 'High'), 1008 | ] 1009 | return [] 1010 | 1011 | 1012 | def asynch_scan(self): 1013 | if not isOk(self.info_response.getStatusCode()): 1014 | return [] 1015 | 1016 | param = helpers.buildParameter("asynch", "true", IParameter.PARAM_URL) 1017 | request = safe_bytes_to_string( helpers.addParameter(self.request, param) ) 1018 | 1019 | newPair = callbacks.makeHttpRequest(self.httpService, request) 1020 | response_modif = safe_bytes_to_string( newPair.getResponse() ) 1021 | 1022 | debug2console("Asynch Scan", request, response_modif) 1023 | 1024 | if get_response_status_code(response_modif) == 202 and \ 1025 | "asynch" in get_response_headers_as_dict(response_modif).get('Location',''): 1026 | 1027 | return [ 1028 | ScanIssue(self.httpService, self.URL, [ newPair ], 1029 | "RESTEasy application supports async jobs", 1030 | ResteasyScanner.DESCR_ASYNCH_SCAN, 1031 | 'Firm', 'Medium'), 1032 | ] 1033 | return [] 1034 | 1035 | 1036 | def url_mapping_scan(self): 1037 | """ Possible names of URL parameter for media type mapping """ 1038 | param_names = [ 1039 | "accept", 1040 | "_accept", 1041 | "__accept", 1042 | "format", 1043 | "_format", 1044 | "__format", 1045 | "ctype", 1046 | "_ctype", 1047 | "__ctype" 1048 | ] 1049 | 1050 | if not isOk(self.info_response.getStatusCode()): 1051 | return [] 1052 | 1053 | for name in param_names: 1054 | param = helpers.buildParameter(name, get_random_string(10), IParameter.PARAM_URL) 1055 | _request = safe_bytes_to_string( helpers.addParameter(self.request, param) ) 1056 | 1057 | newPair = callbacks.makeHttpRequest(self.httpService, _request) 1058 | response_modif = safe_bytes_to_string(newPair.getResponse()) 1059 | 1060 | debug2console("URL Mapping Scan", _request, response_modif) 1061 | 1062 | if get_response_status_code(response_modif) == 500: 1063 | return [ 1064 | ScanIssue(self.httpService, self.URL, [ newPair ], 1065 | "RESTEasy application supports content negotiation via URL parameter", 1066 | ResteasyScanner.DECSR_URL_PARAM_CT_MAPPING_SCAN, 1067 | 'Firm', 'Medium'), 1068 | ] 1069 | return [] --------------------------------------------------------------------------------