├── images ├── image1.png ├── image2.png └── image3.png ├── BappManifest.bmf ├── LICENCE ├── BappDescription.html ├── README.md └── Anti-CSRF_token_from_referer.py /images/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/anti-csrf-token-from-referer/master/images/image1.png -------------------------------------------------------------------------------- /images/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/anti-csrf-token-from-referer/master/images/image2.png -------------------------------------------------------------------------------- /images/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/anti-csrf-token-from-referer/master/images/image3.png -------------------------------------------------------------------------------- /BappManifest.bmf: -------------------------------------------------------------------------------- 1 | Uuid: 8eead913e28549f3aad177303c36e6e5 2 | ExtensionType: 2 3 | Name: Anti-CSRF Token From Referer 4 | RepoName: anti-csrf-token-from-referer 5 | ScreenVersion: 0.1 6 | SerialVersion: 5 7 | MinPlatformVersion: 0 8 | ProOnly: False 9 | Author: Compass Security 10 | ShortDescription: Automatically takes care of anti-CSRF tokens by fetching them from the referer and replacing them in requests. 11 | EntryPoint: Anti-CSRF_token_from_referer.py 12 | BuildCommand: 13 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2020 Thierry Viaccoz and Sylvain Heiniger 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 | -------------------------------------------------------------------------------- /BappDescription.html: -------------------------------------------------------------------------------- 1 |
Working
2 |The extension works by registering a new session handling rule called "Anti-CSRF token from referer". 3 | This handler inspects all the requests and searches for the anti-CSRF token parameter (in GET, POST, JSON and multipart requests) determine$ 4 |
Once this parameter is found, a GET request is issued to the referer of the original request. In this request, all the headers are copied from the original request (e.g Cookie, Authorization...).
5 |This response is then parsed and the substring between the start and the end markers configured in the tab is extracted as the anti-CSRF token.
6 |Finally, the anti-CSRF token is replaced in the original request.
7 |Usage
8 |For further information, consult the documentation on https://github.com/CompassSecurity/anti-csrf-token-from-referer/blob/master/README.md
14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anti-CSRF token from referer 2 | Anti-CSRF token from referer is a Burp Suite extension to automatically take care of anti-CSRF tokens by fetching them from the referer and replacing them in requests. 3 | 4 | ## Installation 5 | The extension can be installed directly from the Burp Suite by going to the "Extender", then "BApp Store" tab, selecting the "Anti-CSRF token from referer" extension and click on "Install". 6 | 7 | It can also be installed manually by downloading [the source code](Anti-CSRF_token_from_referer.py) and clicking on the "Add" button on the "Extender" tab. 8 | 9 | ## Usage 10 | First of all, install the extension as explained in the previous chapter. 11 | 12 | Then, create a new session handling rule: 13 | 1. Open the "Project options" tab 14 | 1. Go to "Sessions" 15 | 1. In "Session Handling Rules", click on "Add" 16 | 1. In "Rule Actions", click on "Add" 17 | 1. Choose "Invoke a Burp extension" 18 | 1. Choose "Anti-CSRF token from referer" 19 | 1. Click on "OK" 20 | 21 |  22 | 23 | Continue by defining the scope: 24 | 1. Change to the the "Scope" tab 25 | 1. Define "Tools Scope", e.g. select "Extender" if you want it to be applied for other extensions as well 26 | 1. Define "URL Scope" 27 | 1. Click on "OK" 28 | 29 |  30 | 31 | End by configuring the parameters: 32 | 1. Open the "Anti-CSRF token from referer" tab 33 | 1. Name: Name of the anti-CSRF token that will be searched for in the original request. Can also be a substring of the name, like `csrf` for `my-csrf-token` (useful in case the anti-CSRF token name is not static). 34 | 1. Start and end markers: Markers defining the start and the end for extracting the anti-CSRF token in the response. Can include the placeholder `#csrf_name#`, which is then replaced by the full name of the anti-CSRF token found in the original request (useful in case the anti-CSRF token name is not static). 35 | 1. Decode and encode: Define whether the anti-CSRF token has to be decoded and/or encoded before usage. 36 | 37 |  38 | 39 | ## Working 40 | The extension works by registering a new session handling rule called "Anti-CSRF token from referer". This handler inspects all the requests and searches for the anti-CSRF token parameter (in GET, POST, JSON and multipart requests) determined by the name configured in the tab. 41 | 42 | Once this parameter is found, a GET request is issued to the referer of the original request. In this request, all the headers are copied from the original request (e.g Cookie, Authorization...). 43 | 44 | This response is then parsed and the substring between the start and the end markers configured in the tab is extracted as the anti-CSRF token. 45 | 46 | Finally, the anti-CSRF token is replaced in the original request. 47 | 48 | ## Example 49 | For the example, suppose that the following request is sent and is being handled by the extension, where the name of the anti-CSRF token is configured as `csrf`: 50 | ``` 51 | POST /submit HTTP/1.1 52 | Host: www.example.com 53 | Referer: https://www.example.com/form 54 | Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF 55 | Content-Type: application/x-www-form-urlencoded 56 | Content-Length: 67 57 | 58 | email=me@example.com&my-csrf-token=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv 59 | ``` 60 | 61 | The extension will notice that the parameter `my-csrf-token` contains the name of the anti-CSRF token `csrf`. Therefore, it will send a GET request to the referer `https://www.example.com/form` with all the headers of the original request (except Host, Content-Length and Content-Type): 62 | ``` 63 | GET /form HTTP/1.1 64 | Host: www.example.com 65 | Referer: https://www.example.com/form 66 | Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF 67 | ``` 68 | 69 | Afterwards, it will extract the anti-CSRF token from the response as configured by the start and end markers (where `#csrf_name#` will be `my-csrf-token`) and replace it in the original request. 70 | 71 | ## Authors 72 | This extension was written by [Thierry Viaccoz](https://github.com/viaccoz) and [Sylvain Heiniger](https://github.com/sploutchy). 73 | 74 | ## Changes 75 | 76 | ### 0.1 77 | * Initial release 78 | -------------------------------------------------------------------------------- /Anti-CSRF_token_from_referer.py: -------------------------------------------------------------------------------- 1 | from burp import IBurpExtender, IParameter, IRequestInfo, ISessionHandlingAction, ITab 2 | from java.net import URL 3 | from HTMLParser import HTMLParser 4 | from java.awt import GridBagConstraints, GridBagLayout 5 | from javax.swing import JCheckBox, JLabel, JPanel, JTextField 6 | from logging import Formatter, StreamHandler, getLogger, ERROR, INFO, DEBUG 7 | from sys import stdout 8 | 9 | EXTENSION_NAME = 'Anti-CSRF token from referer' 10 | EXTENSION_VERSION = '0.1' 11 | EXTENSION_NAME_VERSION = EXTENSION_NAME + ' ' + EXTENSION_VERSION 12 | 13 | # Configure the logger 14 | LOG_LEVEL = ERROR 15 | FMT = '%(asctime)s:%(msecs)03d [%(levelname)s] %(message)s' 16 | DATEFMT = '%H:%M:%S' 17 | 18 | # Configure the constants 19 | CSRF_NAME_PLACEHOLDER = '#csrf_name#' 20 | HEADER_NAME_VALUE_SEPARATOR = ': ' 21 | REFERER_HEADER_NAME = 'Referer' 22 | HEADER_NAMES_TO_EXCLUDE = ['Host', 'Content-Length', 'Content-Type'] 23 | NEWLINE = '\r\n' 24 | 25 | class BurpExtender(IBurpExtender, ISessionHandlingAction, ITab): 26 | def registerExtenderCallbacks(self, callbacks): 27 | """IBurpExtender""" 28 | callbacks.setExtensionName(EXTENSION_NAME) 29 | 30 | self._helpers = callbacks.getHelpers() 31 | self._callbacks = callbacks 32 | self._html_parser = HTMLParser() 33 | 34 | self._logger = getLogger(__name__) 35 | self.initialize_logger() 36 | 37 | self.build_gui() 38 | 39 | callbacks.registerSessionHandlingAction(self) 40 | callbacks.addSuiteTab(self) 41 | 42 | self._logger.info(EXTENSION_NAME_VERSION) 43 | 44 | def getActionName(self): 45 | """ISessionHandlingAction""" 46 | return EXTENSION_NAME 47 | 48 | def performAction(self, current_request, macro_items): 49 | """ISessionHandlingAction""" 50 | # Read the configured values 51 | csrf_name_contains = self._csrf_name_contains_field.text 52 | csrf_start_marker = self._csrf_start_marker_field.text 53 | csrf_end_marker = self._csrf_end_marker_field.text 54 | do_html_decode = self._do_html_decode.isSelected() 55 | do_url_decode = self._do_url_decode.isSelected() 56 | do_url_encode = self._do_url_encode.isSelected() 57 | 58 | self._logger.debug('Name of the anti-CSRF token (or part of): %s', csrf_name_contains) 59 | self._logger.debug('Start marker of the anti-CSRF token in the response: %s', csrf_start_marker) 60 | self._logger.debug('End marker of the anti-CSRF token in the response: %s', csrf_end_marker) 61 | self._logger.debug('HTML decode the anti-CSRF token: %s', do_html_decode) 62 | self._logger.debug('URL decode the anti-CSRF token: %s', do_url_decode) 63 | self._logger.debug('URL encode the anti-CSRF token: %s', do_url_encode) 64 | 65 | request_info = self._helpers.analyzeRequest(current_request) 66 | self._logger.info('Handling request to: %s %s', request_info.getMethod(), request_info.getUrl().toString()) 67 | 68 | # Iterate over the parameters to find the first anti-CSRF one 69 | csrf_name = None 70 | csrf_type = None 71 | for parameter in request_info.getParameters(): 72 | parameter_name = parameter.getName() 73 | parameter_type = parameter.getType() 74 | self._logger.debug('Parameter found: (%s, %d)', parameter_name, parameter_type) 75 | 76 | if csrf_name_contains in parameter_name: 77 | csrf_name = parameter_name 78 | csrf_type = parameter_type 79 | self._logger.info('Anti-CSRF parameter found: %s', parameter_name) 80 | break 81 | 82 | if csrf_name is None or csrf_type is None: 83 | self._logger.info('No anti-CSRF token parameter in request') 84 | return 85 | 86 | # Iterate over the headers to find the referer and to extract the headers to copy 87 | get_csrf_request_headers = "" 88 | referer_url = None 89 | for header in request_info.getHeaders(): 90 | self._logger.debug('Header found: %s', header) 91 | 92 | header_split = header.split(HEADER_NAME_VALUE_SEPARATOR, 1) 93 | if len(header_split) < 2: 94 | continue 95 | 96 | header_name = header_split[0] 97 | header_value = header_split[1] 98 | 99 | # We first look for the referer, so it will never be copied in the request to the referer and thus avoids loops 100 | if header_name == REFERER_HEADER_NAME: 101 | referer_url = header_value 102 | self._logger.debug('Referer URL found') 103 | elif header_name not in HEADER_NAMES_TO_EXCLUDE: 104 | get_csrf_request_headers += header + NEWLINE 105 | 106 | if referer_url is None: 107 | self._logger.info('No referer URL found') 108 | return 109 | 110 | # Build a GET request to the referer URL 111 | get_csrf_request = self._helpers.buildHttpRequest(URL(referer_url)) 112 | get_csrf_request = self.delete_headers(get_csrf_request) 113 | get_csrf_request += self._helpers.stringToBytes(get_csrf_request_headers + NEWLINE) 114 | 115 | # Make the request 116 | self._logger.info('Request for anti-CSRF request to: %s', referer_url) 117 | get_csrf_response = self._callbacks.makeHttpRequest(current_request.getHttpService(), get_csrf_request).getResponse() 118 | 119 | # Replace the anti-CSRF token name in the anti-CSRF start and end markers 120 | csrf_start_marker = csrf_start_marker.replace(CSRF_NAME_PLACEHOLDER, csrf_name) 121 | csrf_end_marker = csrf_end_marker.replace(CSRF_NAME_PLACEHOLDER, csrf_name) 122 | self._logger.debug('Extract data from string %s to string %s', csrf_start_marker, csrf_end_marker) 123 | 124 | # Extract anti-CSRF value from the response 125 | csrf_value = self.extract_by_markers(get_csrf_response, csrf_start_marker, csrf_end_marker) 126 | if csrf_value == None: 127 | self._logger.error('No anti-CSRF token parameter found in response') 128 | return 129 | 130 | self._logger.debug('Anti-CSRF token parameter value before decoding and encoding: %s', csrf_value) 131 | 132 | if do_html_decode: 133 | self._logger.debug('Perform HTML decode') 134 | csrf_value = self._html_parser.unescape(csrf_value) 135 | 136 | if do_url_decode: 137 | self._logger.debug('Perform URL decode') 138 | csrf_value = self._helpers.urlDecode(csrf_value) 139 | 140 | if do_url_encode: 141 | self._logger.debug('Perform URL encode') 142 | csrf_value = self._helpers.urlEncode(csrf_value) 143 | 144 | self._logger.debug('Anti-CSRF token parameter value after decoding and encoding: %s', csrf_value) 145 | 146 | # Build a new parameter with the updated value of the anti-CSRF token 147 | csrf_parameter = self._helpers.buildParameter(csrf_name, csrf_value, csrf_type) 148 | 149 | # Let the original request go throuth 150 | current_request.setRequest(self._helpers.updateParameter(current_request.getRequest(), csrf_parameter)) 151 | self._logger.info('Anti-CSRF token value replaced') 152 | 153 | def getTabCaption(self): 154 | """ITab""" 155 | return EXTENSION_NAME 156 | 157 | def getUiComponent(self): 158 | """ITab""" 159 | return self._ui_component 160 | 161 | def initialize_logger(self): 162 | formatter = Formatter(fmt=FMT, datefmt=DATEFMT) 163 | 164 | handler = StreamHandler(stream=stdout) 165 | handler.setFormatter(formatter) 166 | 167 | self._logger.addHandler(handler) 168 | self._logger.setLevel(LOG_LEVEL) 169 | 170 | def build_gui(self): 171 | component = JPanel(GridBagLayout()) 172 | c = GridBagConstraints() 173 | c.fill = GridBagConstraints.HORIZONTAL 174 | 175 | c.gridwidth = 2 176 | 177 | # 1st line 178 | c.gridy = 0 179 | 180 | c.gridx = 0 181 | component.add(JLabel('Don\'t forget to create a Session handling rule invoking this Burp extension under Project options > Sessions and to configure the tool and URL scopes.'), c) 182 | 183 | c.gridwidth = 1 184 | 185 | # 2nd line 186 | c.gridy = 1 187 | 188 | c.gridx = 0 189 | component.add(JLabel(' '), c) 190 | 191 | # 3rd line 192 | c.gridy = 2 193 | 194 | c.gridx = 0 195 | component.add(JLabel('Name of the anti-CSRF token, can also be a substring of the name of the anti-CSRF token:'), c) 196 | 197 | self._csrf_name_contains_field = JTextField('csrf', 40) 198 | c.gridx = 1 199 | component.add(self._csrf_name_contains_field, c) 200 | 201 | # 4th line 202 | c.gridy = 3 203 | 204 | c.gridx = 0 205 | component.add(JLabel('Start marker of the anti-CSRF token in the response, #csrf_name# is replaced by the name of the anti-CSRF token:'), c) 206 | 207 | self._csrf_start_marker_field = JTextField('name="' + CSRF_NAME_PLACEHOLDER + '" value="', 40) 208 | c.gridx = 1 209 | component.add(self._csrf_start_marker_field, c) 210 | 211 | # 5th line 212 | c.gridy = 4 213 | 214 | c.gridx = 0 215 | component.add(JLabel('End marker of the anti-CSRF token in the response, #csrf_name# is replaced by the name of the anti-CSRF token:'), c) 216 | 217 | self._csrf_end_marker_field = JTextField('"', 40) 218 | c.gridx = 1 219 | component.add(self._csrf_end_marker_field, c) 220 | 221 | # 6th line 222 | c.gridy = 5 223 | 224 | c.gridx = 0 225 | component.add(JLabel(' '), c) 226 | 227 | # 7th line 228 | c.gridy = 6 229 | 230 | c.gridx = 0 231 | component.add(JLabel('HTML decode the anti-CSRF token:'), c) 232 | 233 | self._do_html_decode = JCheckBox("", True) 234 | c.gridx = 1 235 | component.add(self._do_html_decode, c) 236 | 237 | # 8th line 238 | c.gridy = 7 239 | 240 | c.gridx = 0 241 | component.add(JLabel('URL decode the anti-CSRF token:'), c) 242 | 243 | self._do_url_decode = JCheckBox("", True) 244 | c.gridx = 1 245 | component.add(self._do_url_decode, c) 246 | 247 | # 9th line 248 | c.gridy = 8 249 | 250 | c.gridx = 0 251 | component.add(JLabel('URL encode the anti-CSRF token:'), c) 252 | 253 | self._do_url_encode = JCheckBox("", True) 254 | c.gridx = 1 255 | component.add(self._do_url_encode, c) 256 | 257 | # 10th line 258 | c.gridy = 9 259 | 260 | c.gridx = 0 261 | component.add(JLabel(' '), c) 262 | 263 | self._callbacks.customizeUiComponent(component) 264 | self._ui_component = component 265 | 266 | def delete_headers(self, request): 267 | """Delete all the headers except the first line (e.g. GET...) and the host header""" 268 | newline_bytes = self._helpers.stringToBytes(NEWLINE) 269 | first_line_index = self._helpers.indexOf(request, newline_bytes, False, 0, len(request)) 270 | first_line_index += len(newline_bytes) 271 | second_line_index = self._helpers.indexOf(request, newline_bytes, False, first_line_index, len(request)) 272 | second_line_index += len(newline_bytes) 273 | return request[:second_line_index] 274 | 275 | def extract_by_markers(self, data, start_marker, end_marker): 276 | """Extract the content between start_marker and end_marker in data""" 277 | start_marker_bytes= self._helpers.stringToBytes(start_marker) 278 | end_marker_bytes = self._helpers.stringToBytes(end_marker) 279 | 280 | start_index = self._helpers.indexOf(data, start_marker_bytes, False, 0, len(data)) 281 | if start_index == -1: 282 | self._logger.debug('Start marker not found') 283 | return None 284 | 285 | start_index += len(start_marker_bytes) 286 | 287 | end_index = self._helpers.indexOf(data, end_marker_bytes, False, start_index, len(data)) 288 | if end_index == -1: 289 | self._logger.debug('End marker not found') 290 | return None 291 | 292 | self._logger.debug('Extract data from index %d to index %d', start_index, end_index) 293 | return self._helpers.bytesToString(data[start_index:end_index]) 294 | --------------------------------------------------------------------------------