├── 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 []
--------------------------------------------------------------------------------