├── .gitignore ├── payloads.txt ├── test └── test.txt ├── LICENSE ├── README.md └── jsonp.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | -------------------------------------------------------------------------------- /payloads.txt: -------------------------------------------------------------------------------- 1 | .jsonp?callback=test 2 | .jsonp 3 | ?callback=test 4 | ?jsonp=test -------------------------------------------------------------------------------- /test/test.txt: -------------------------------------------------------------------------------- 1 | http://example.com/path/?asd=test 2 | http://example.com/path.json/?asd=test 3 | http://example.com/path.json?asd=test&asd=test 4 | http://example.com/path/ 5 | http://example.com/path.json/ 6 | http://example.com/path?asd=test&test=lol 7 | http://example.com/path 8 | 9 | unit tests SOON ™ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 kapytein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonp 2 | 3 | ![alt](https://www.upload.ee/image/10396748/Screenshot_from_2019-08-24_23-39-07.png) 4 | 5 | jsonp is a Burp Extension which tries to discover JSONP functionality behind JSON endpoints. It does so by appending parameters and/or changing the extension of the requested URL. The payloads are taken from payloads.txt. 6 | 7 | The extension acts as a passive scanner (while it actually is not, since it creates requests based on the original request). For every request responding with `application/json`, the plugin will send `4` altered requests, using the payloads from `payloads.txt`. Only the request path and method will be altered. All requests made by the plugin are using the request method `GET`. 8 | 9 | JSONP functionalities (if not restricted) could be used to bypass content security policies. Besides that, in case there's authenticated data, you could attempt a cross-site script inclusion attack if no CSRF token or equivalent is used to migitate the exploitability. 10 | 11 | It's common that JSONP functionalities are hidden behind JSON endpoints, as learned on [Liberapay](https://hackerone.com/reports/361951). The template rendered using `jsonp_dump`, which would return valid JSON with content type `application/json` when no `callback` parameter is supplied. 12 | 13 | ## Installation 14 | 15 | The extension is currently not in the BApp Store. You have to install it manually via "Extender > Add". 16 | 17 | ## Common false-positivies for exploitability 18 | The extension uses the cookies and (possibly additional) authentication headers from the original request. This means that the extension does not detect whether the JSONP functionality on the endpoint is exploitable or not. 19 | -------------------------------------------------------------------------------- /jsonp.py: -------------------------------------------------------------------------------- 1 | from burp import IBurpExtender 2 | from burp import IScannerCheck 3 | from burp import IScanIssue 4 | from burp import IExtensionHelpers 5 | from java.net import URL 6 | from array import array 7 | from urlparse import urlparse 8 | import sys 9 | import os 10 | 11 | class BurpExtender(IBurpExtender, IScannerCheck): 12 | 13 | def registerExtenderCallbacks(self, callbacks): 14 | # keep a reference to our callbacks object 15 | self._callbacks = callbacks 16 | 17 | # obtain an extension helpers object 18 | self._helpers = callbacks.getHelpers() 19 | 20 | # set our extension name 21 | callbacks.setExtensionName("jsonp") 22 | 23 | # register ourselves as a custom scanner check 24 | callbacks.registerScannerCheck(self) 25 | 26 | sys.stdout = callbacks.getStdout() 27 | sys.stderr = callbacks.getStderr() 28 | 29 | def load_payloads(self): 30 | lines = [] 31 | with open('payloads.txt') as f: 32 | lines = f.read().splitlines() 33 | 34 | return lines 35 | 36 | ''' 37 | https://stackoverflow.com/questions/3675318/how-to-replace-the-some-characters-from-the-end-of-a-string 38 | ''' 39 | def replace_last(self, source_string, replace_what, replace_with): 40 | head, _sep, tail = source_string.rpartition(replace_what) 41 | return head + replace_with + tail 42 | 43 | def remove_parameters(self, url): 44 | u = urlparse(url) 45 | query = "?" + u.query 46 | return url.replace(query, '') 47 | 48 | ''' 49 | The function attempts to place the payload within the requested URL. A payload consists of an extension and query parameters only. 50 | URL's end often (without query parameters / fragment) with a slash (/) or without a slash. Both scenarios are currently covered by this function. 51 | ''' 52 | def construct_url(self, url, payload): 53 | has_slash = False 54 | org_url = urlparse(url) 55 | 56 | url = self.remove_parameters(url) 57 | 58 | if url.endswith("/"): 59 | has_slash = True 60 | url = self.replace_last(url, '/', '') 61 | 62 | u = urlparse(url) 63 | url_ext = os.path.splitext(u.path)[1] 64 | payload_ext = urlparse(payload) 65 | 66 | # we have an ext in the payload 67 | if payload_ext.path != "": 68 | if url_ext != "": 69 | url = self.replace_last(url, url_ext, payload_ext.path) 70 | payload = payload.replace(payload_ext.path, '') 71 | 72 | elif has_slash == True and url_ext == "": 73 | # place payload ext before the / 74 | url = url + payload_ext.path 75 | payload = payload.replace(payload_ext.path, '') 76 | 77 | if has_slash == True: 78 | url = url + "/" 79 | 80 | if org_url.query != "": 81 | if payload_ext.query != "": 82 | payload = payload + "&" + org_url.query 83 | else: 84 | payload = payload + "?" + org_url.query 85 | 86 | return url + payload 87 | 88 | def replace_header(self, headers, value): 89 | # the request method will always be the first value in the list 90 | headers[0] = value 91 | return headers 92 | 93 | def doPassiveScan(self, baseRequestResponse): 94 | response = baseRequestResponse.getResponse() 95 | 96 | res_type = self._helpers.analyzeResponse(response).getStatedMimeType() 97 | if res_type == "JSON": 98 | payloads = self.load_payloads() 99 | 100 | for i in payloads: 101 | request_url = self._helpers.analyzeRequest(baseRequestResponse).getUrl() 102 | payload_url = urlparse(self.construct_url(str(request_url), i)) 103 | 104 | if payload_url.query != "": 105 | payload_format = '{uri.path}?{uri.query}'.format(uri=payload_url) 106 | else: 107 | payload_format = '{uri.path}'.format(uri=payload_url) 108 | 109 | request_headers = self.replace_header(self._helpers.analyzeRequest(baseRequestResponse).getHeaders(), "GET " + payload_format + " HTTP/1.1") 110 | 111 | request = self._helpers.buildHttpMessage(request_headers, None) 112 | print("Edited URL, and creating request to the following URL: " + payload_format) 113 | 114 | response = self._callbacks.makeHttpRequest(request_url.getHost(), request_url.getPort(), False if request_url.getProtocol() == "http" else True, request) 115 | response_type = self._helpers.analyzeResponse(response).getStatedMimeType() 116 | 117 | if response_type == "script": 118 | 119 | return [CustomScanIssue( 120 | baseRequestResponse.getHttpService(), 121 | self._helpers.analyzeRequest(baseRequestResponse).getUrl(), 122 | [baseRequestResponse], 123 | "Hidden JSONP endpoint found", 124 | # @TODO A class which implements IHttpRequestResponse needs to be created for a byte > ihttprequestresponse conversion. There's no helper for this 125 | "Callback request path: " + payload_format + ". A JSON endpoint was found with a (possibly hidden) JSONP functionality. This allows you to retrieve the returned data cross-origin (in case there are no additional checks / CSRF tokens in place). This may also help to bypass content security policies.", 126 | "Medium")] 127 | 128 | def consolidateDuplicateIssues(self, existingIssue, newIssue): 129 | # This method is called when multiple issues are reported for the same URL 130 | # path by the same extension-provided check. The value we return from this 131 | # method determines how/whether Burp consolidates the multiple issues 132 | # to prevent duplication 133 | # 134 | # Since the issue name is sufficient to identify our issues as different, 135 | # if both issues have the same name, only report the existing issue 136 | # otherwise report both issues 137 | if existingIssue.getIssueName() == newIssue.getIssueName(): 138 | return -1 139 | 140 | return 0 141 | 142 | class CustomScanIssue(IScanIssue): 143 | def __init__(self, httpService, url, httpMessages, name, detail, severity): 144 | self._httpService = httpService 145 | self._url = url 146 | self._httpMessages = httpMessages 147 | self._name = name 148 | self._detail = detail 149 | self._severity = severity 150 | 151 | def getUrl(self): 152 | return self._url 153 | 154 | def getIssueName(self): 155 | return self._name 156 | 157 | def getIssueType(self): 158 | return 0 159 | 160 | def getSeverity(self): 161 | return self._severity 162 | 163 | def getConfidence(self): 164 | return "Certain" 165 | 166 | def getIssueBackground(self): 167 | pass 168 | 169 | def getRemediationBackground(self): 170 | pass 171 | 172 | def getIssueDetail(self): 173 | return self._detail 174 | 175 | def getRemediationDetail(self): 176 | pass 177 | 178 | def getHttpMessages(self): 179 | return self._httpMessages 180 | 181 | def getHttpService(self): 182 | return self._httpService --------------------------------------------------------------------------------