├── README.md ├── java ├── .gitignore ├── BurpExtender.java ├── build.gradle └── settings.gradle ├── python └── CustomScanInsertionPoint.py ├── ruby └── CustomScanInsertionPoint.rb ├── server ├── Demo.aspx └── server.js ├── xss1.png ├── xss2.png └── xss3.png /README.md: -------------------------------------------------------------------------------- 1 | # Sample Burp Suite extension: custom scan insertion points 2 | 3 | In the [custom editor tab example](//github.com/PortSwigger/example-custom-editor-tab), we 4 | saw how a simple Burp extension could be used to render and edit a custom 5 | message data format, within the Burp UI. For the purpose of demonstrating this 6 | capability, we used a trivial serialization format, in which user-supplied 7 | input is Base64-encoded within a request parameter value. 8 | 9 | That example contained a rather obvious XSS vulnerability: the raw input 10 | contained within the serialized data is echoed unfiltered in the application's 11 | response. But although this type of bug might be obvious to a human, automated 12 | scanners will not (in general) identify any kinds of input-based vulnerabilities 13 | in cases where the raw input needs to be embedded within an unsupported 14 | serialization format. Since the scanner does not understand the format, it has 15 | no means of submitting its usual scan payloads in the way that is needed for the 16 | application to unpack and process the payloads and trigger any bugs. This means 17 | that in this situation, equipped only with the [example of a custom editor tab 18 | extension](//github.com/PortSwigger/example-custom-editor-tab), you would be 19 | restricted to manual testing for input-based bugs, which is a tedious and 20 | time-consuming process. 21 | 22 | The [extender API](https://portswigger.net/burp/extender/) lets you tackle this 23 | problem by registering your extension as a provider of custom scanner insertion 24 | points. For each actively scanned request, Burp will call out to your 25 | extension, and ask it to provide any custom insertion points that are 26 | applicable to the request. Each insertion point that you provide is responsible 27 | for the job of constructing validly-formed requests for specific scan payloads. 28 | This lets your extension work with any data format, and embed the scanner's 29 | payloads within the request in the correct way. 30 | 31 | Here, we can see Burp reporting the XSS vulnerability, which it has found via 32 | the custom "Base64-wrapped input" insertion point: 33 | 34 | ![xss1](xss1.png) 35 | 36 | Here is the request that Burp made, and which was generated for Burp by our 37 | custom insertion point: 38 | 39 | ![xss2](xss2.png) 40 | 41 | Here, via our custom message editor tab, is the literal scan payload that is 42 | embedded in the request: 43 | 44 | ![xss3](xss3.png) 45 | 46 | So, with a few lines of extension code, we have taught Burp Scanner how to work 47 | with the unsupported serialization format. All of Burp's built-in scan checks 48 | can now place their payloads correctly into the application's requests, and 49 | bugs like this can be quickly found. 50 | 51 | This repository includes source code for Java, Python and Ruby. It also contains 52 | a sample server (for ASP.NET and NodeJS) to execute a scan against. 53 | -------------------------------------------------------------------------------- /java/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | -------------------------------------------------------------------------------- /java/BurpExtender.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class BurpExtender implements IBurpExtender, IScannerInsertionPointProvider 7 | { 8 | private IExtensionHelpers helpers; 9 | 10 | // 11 | // implement IBurpExtender 12 | // 13 | 14 | @Override 15 | public void registerExtenderCallbacks(final IBurpExtenderCallbacks callbacks) 16 | { 17 | // obtain an extension helpers object 18 | helpers = callbacks.getHelpers(); 19 | 20 | // set our extension name 21 | callbacks.setExtensionName("Serialized input scan insertion point"); 22 | 23 | // register ourselves as a scanner insertion point provider 24 | callbacks.registerScannerInsertionPointProvider(this); 25 | } 26 | 27 | // 28 | // implement IScannerInsertionPointProvider 29 | // 30 | 31 | @Override 32 | public List getInsertionPoints(IHttpRequestResponse baseRequestResponse) 33 | { 34 | // retrieve the data parameter 35 | IParameter dataParameter = helpers.getRequestParameter(baseRequestResponse.getRequest(), "data"); 36 | if (dataParameter == null) 37 | return null; 38 | 39 | // if the parameter is present, add a single custom insertion point for it 40 | List insertionPoints = new ArrayList(); 41 | insertionPoints.add(new InsertionPoint(baseRequestResponse.getRequest(), dataParameter.getValue())); 42 | return insertionPoints; 43 | } 44 | 45 | // 46 | // class implementing IScannerInsertionPoint 47 | // 48 | private class InsertionPoint implements IScannerInsertionPoint 49 | { 50 | private byte[] baseRequest; 51 | private String insertionPointPrefix; 52 | private String baseValue; 53 | private String insertionPointSuffix; 54 | 55 | InsertionPoint(byte[] baseRequest, String dataParameter) 56 | { 57 | this.baseRequest = baseRequest; 58 | 59 | // URL- and base64-decode the data 60 | dataParameter = helpers.bytesToString(helpers.base64Decode(helpers.urlDecode(dataParameter))); 61 | 62 | // parse the location of the input string within the decoded data 63 | int start = dataParameter.indexOf("input=") + 6; 64 | insertionPointPrefix = dataParameter.substring(0, start); 65 | int end = dataParameter.indexOf("&", start); 66 | if (end == -1) 67 | end = dataParameter.length(); 68 | baseValue = dataParameter.substring(start, end); 69 | insertionPointSuffix = dataParameter.substring(end, dataParameter.length()); 70 | } 71 | 72 | // 73 | // implement IScannerInsertionPoint 74 | // 75 | 76 | @Override 77 | public String getInsertionPointName() 78 | { 79 | return "Base64-wrapped input"; 80 | } 81 | 82 | @Override 83 | public String getBaseValue() 84 | { 85 | return baseValue; 86 | } 87 | 88 | @Override 89 | public byte[] buildRequest(byte[] payload) 90 | { 91 | // build the raw data using the specified payload 92 | String input = insertionPointPrefix + helpers.bytesToString(payload) + insertionPointSuffix; 93 | 94 | // Base64- and URL-encode the data 95 | input = helpers.urlEncode(helpers.base64Encode(input)); 96 | 97 | // update the request with the new parameter value 98 | return helpers.updateParameter(baseRequest, helpers.buildParameter("data", input, IParameter.PARAM_BODY)); 99 | } 100 | 101 | @Override 102 | public int[] getPayloadOffsets(byte[] payload) 103 | { 104 | // since the payload is being inserted into a serialized data structure, there aren't any offsets 105 | // into the request where the payload literally appears 106 | return null; 107 | } 108 | 109 | @Override 110 | public byte getInsertionPointType() 111 | { 112 | return INS_EXTENSION_PROVIDED; 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /java/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | compile 'net.portswigger.burp.extender:burp-extender-api:1.7.13' 9 | } 10 | 11 | sourceSets { 12 | main { 13 | java { 14 | srcDir '.' 15 | } 16 | } 17 | } 18 | 19 | task fatJar(type: Jar) { 20 | baseName = project.name + '-all' 21 | from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } 22 | with jar 23 | } 24 | -------------------------------------------------------------------------------- /java/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'CustomScanInsertionPoint' 2 | -------------------------------------------------------------------------------- /python/CustomScanInsertionPoint.py: -------------------------------------------------------------------------------- 1 | from burp import IBurpExtender 2 | from burp import IScannerInsertionPointProvider 3 | from burp import IScannerInsertionPoint 4 | from burp import IParameter 5 | import string 6 | 7 | class BurpExtender(IBurpExtender, IScannerInsertionPointProvider): 8 | 9 | # 10 | # implement IBurpExtender 11 | # 12 | 13 | def registerExtenderCallbacks(self, callbacks): 14 | 15 | # obtain an extension helpers object 16 | self._helpers = callbacks.getHelpers() 17 | 18 | # set our extension name 19 | callbacks.setExtensionName("Serialized input scan insertion point") 20 | 21 | # register ourselves as a scanner insertion point provider 22 | callbacks.registerScannerInsertionPointProvider(self) 23 | 24 | return 25 | 26 | # 27 | # implement IScannerInsertionPointProvider 28 | # 29 | 30 | def getInsertionPoints(self, baseRequestResponse): 31 | 32 | # retrieve the data parameter 33 | dataParameter = self._helpers.getRequestParameter(baseRequestResponse.getRequest(), "data") 34 | if (dataParameter is None): 35 | return None 36 | 37 | else: 38 | # if the parameter is present, add a single custom insertion point for it 39 | return [ InsertionPoint(self._helpers, baseRequestResponse.getRequest(), dataParameter.getValue()) ] 40 | 41 | # 42 | # class implementing IScannerInsertionPoint 43 | # 44 | 45 | class InsertionPoint(IScannerInsertionPoint): 46 | 47 | def __init__(self, helpers, baseRequest, dataParameter): 48 | self._helpers = helpers 49 | self._baseRequest = baseRequest 50 | 51 | # URL- and base64-decode the data 52 | dataParameter = helpers.bytesToString(helpers.base64Decode(helpers.urlDecode(dataParameter))) 53 | 54 | # parse the location of the input string within the decoded data 55 | start = string.find(dataParameter, "input=") + 6 56 | self._insertionPointPrefix = dataParameter[:start] 57 | end = string.find(dataParameter, "&", start) 58 | if (end == -1): 59 | end = dataParameter.length() 60 | self._baseValue = dataParameter[start:end] 61 | self._insertionPointSuffix = dataParameter[end:] 62 | return 63 | 64 | # 65 | # implement IScannerInsertionPoint 66 | # 67 | 68 | def getInsertionPointName(self): 69 | return "Base64-wrapped input" 70 | 71 | def getBaseValue(self): 72 | return self._baseValue 73 | 74 | def buildRequest(self, payload): 75 | # build the raw data using the specified payload 76 | input = self._insertionPointPrefix + self._helpers.bytesToString(payload) + self._insertionPointSuffix; 77 | 78 | # Base64- and URL-encode the data 79 | input = self._helpers.urlEncode(self._helpers.base64Encode(input)); 80 | 81 | # update the request with the new parameter value 82 | return self._helpers.updateParameter(self._baseRequest, self._helpers.buildParameter("data", input, IParameter.PARAM_BODY)) 83 | 84 | def getPayloadOffsets(self, payload): 85 | # since the payload is being inserted into a serialized data structure, there aren't any offsets 86 | # into the request where the payload literally appears 87 | return None 88 | 89 | def getInsertionPointType(self): 90 | return INS_EXTENSION_PROVIDED 91 | -------------------------------------------------------------------------------- /ruby/CustomScanInsertionPoint.rb: -------------------------------------------------------------------------------- 1 | java_import 'burp.IBurpExtender' 2 | java_import 'burp.IScannerInsertionPointProvider' 3 | java_import 'burp.IScannerInsertionPoint' 4 | java_import 'burp.IParameter' 5 | 6 | class BurpExtender 7 | include IBurpExtender, IScannerInsertionPointProvider 8 | 9 | # 10 | # implement IBurpExtender 11 | # 12 | 13 | def registerExtenderCallbacks(callbacks) 14 | # obtain an extension helpers object 15 | @helpers = callbacks.getHelpers 16 | 17 | # set our extension name 18 | callbacks.setExtensionName "Serialized input scan insertion point" 19 | 20 | # register ourselves as a scanner insertion point provider 21 | callbacks.registerScannerInsertionPointProvider self 22 | 23 | return 24 | end 25 | 26 | # 27 | # implement IScannerInsertionPointProvider 28 | # 29 | 30 | def getInsertionPoints(baseRequestResponse) 31 | # retrieve the data parameter 32 | dataParameter = @helpers.getRequestParameter baseRequestResponse.getRequest, "data" 33 | return if dataParameter.nil? 34 | 35 | # if the parameter is present, add a single custom insertion point for it 36 | return [InsertionPoint.new(@helpers, baseRequestResponse.getRequest, dataParameter.getValue)] 37 | end 38 | end 39 | 40 | # 41 | # class implementing IScannerInsertionPoint 42 | # 43 | 44 | class InsertionPoint 45 | include IScannerInsertionPoint 46 | 47 | def initialize(helpers, baseRequest, dataParameter) 48 | @helpers = helpers 49 | @baseRequest = baseRequest 50 | 51 | # URL- and base64-decode the data 52 | dataParameter = helpers.bytesToString(helpers.base64Decode(helpers.urlDecode(dataParameter))) 53 | 54 | # parse the location of the input string within the decoded data 55 | start = dataParameter.index("input=") + 6 56 | @insertionPointPrefix = dataParameter[0...start] 57 | end_ = dataParameter.index("&", start) 58 | end_ = dataParameter.length if end_ == -1 59 | @baseValue = dataParameter[start...end_] 60 | @insertionPointSuffix = dataParameter[end_..-1] 61 | return 62 | end 63 | 64 | # 65 | # implement IScannerInsertionPoint 66 | # 67 | 68 | def getInsertionPointName() 69 | "Base64-wrapped input" 70 | end 71 | 72 | def getBaseValue() 73 | @baseValue 74 | end 75 | 76 | def buildRequest(payload) 77 | # build the raw data using the specified payload 78 | input = @insertionPointPrefix + @helpers.bytesToString(payload) + @insertionPointSuffix 79 | 80 | # Base64- and URL-encode the data 81 | input = @helpers.urlEncode @helpers.base64Encode(input) 82 | 83 | # update the request with the new parameter value 84 | return @helpers.updateParameter(@baseRequest, @helpers.buildParameter("data", input, IParameter.PARAM_BODY)) 85 | end 86 | 87 | def getPayloadOffsets(payload) 88 | # since the payload is being inserted into a serialized data structure, there aren't any offsets 89 | # into the request where the payload literally appears 90 | end 91 | 92 | def getInsertionPointType() 93 | INS_EXTENSION_PROVIDED 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /server/Demo.aspx: -------------------------------------------------------------------------------- 1 | <%@ Page Language="C#" %> 2 | 3 | <% 4 | string data = Request.Form["data"]; 5 | if (data != null) 6 | { 7 | data = Encoding.ASCII.GetString(Convert.FromBase64String(data)); 8 | data = HttpUtility.ParseQueryString(data)["input"]; 9 | output.InnerHtml = "Input received: " + data; 10 | } 11 | %> 12 | 13 | 14 | 15 | 16 | Demo 17 | 26 | 27 | 28 |
29 | Input: 30 |
31 |
32 |
33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | 3 | const PORT = 8000; 4 | 5 | const parseQueryString = str => str 6 | .split('&') 7 | .map(pair => { 8 | const idx = pair.indexOf('='); 9 | if (idx === -1) return null; 10 | return [pair.substr(0, idx), pair.substr(idx+1)]; 11 | }) 12 | .reduce((acc, kvp) => { 13 | if (kvp !== null) acc[unescape(kvp[0])] = unescape(kvp[1]); 14 | return acc; 15 | }, {}); 16 | 17 | console.log(`Serving on http://localhost:${PORT}, press ctrl+c to stop`); 18 | http.createServer((req, res) => { 19 | res.writeHead(200, {'Content-Type': 'text/html'}); 20 | 21 | if (req.method === 'POST') { 22 | const body = []; 23 | req.on('data', chunk => { 24 | body.push(chunk); 25 | }).on('end', () => { 26 | var data = parseQueryString(Buffer.concat(body).toString()).data; 27 | data = new Buffer(data, 'base64').toString('ascii'); 28 | 29 | res.end(`Input received: ${parseQueryString(data).input}`); 30 | }); 31 | } else { 32 | res.end(` 33 | 34 | 35 | 36 | Demo 37 | 46 | 47 | 48 |
49 | Input: 50 |
51 |
52 |
53 | 54 | 55 |
56 |
57 |
58 | 59 | 60 | `); 61 | } 62 | }).listen(PORT, 'localhost'); 63 | -------------------------------------------------------------------------------- /xss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/example-custom-scan-insertion-points/688fd39edb529249976aefc36d53f45aa01568be/xss1.png -------------------------------------------------------------------------------- /xss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/example-custom-scan-insertion-points/688fd39edb529249976aefc36d53f45aa01568be/xss2.png -------------------------------------------------------------------------------- /xss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/example-custom-scan-insertion-points/688fd39edb529249976aefc36d53f45aa01568be/xss3.png --------------------------------------------------------------------------------