diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3721f2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.gradle +build +settings.xml + diff --git a/build.gradle b/build.gradle index 20d1be1..74a9aa1 100644 --- a/build.gradle +++ b/build.gradle @@ -4,24 +4,31 @@ repositories { mavenCentral() } -dependencies { - compile 'net.portswigger.burp.extender:burp-extender-api:1.7.22' - compile 'com.google.code.gson:gson:2.8.6' -} +apply plugin: 'jacoco' -sourceSets { - main { - java { - srcDir 'src' - } - resources { - srcDir 'resources' - } +jacocoTestReport { + reports { + xml.enabled = true + html.enabled = true } } +check.dependsOn jacocoTestReport + +dependencies { + implementation 'net.portswigger.burp.extender:burp-extender-api:1.7.22' + implementation 'com.google.code.gson:gson:2.8.6' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.8.47' +} + task fatJar(type: Jar) { - baseName = project.name + '-all' - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + archiveBaseName = project.name + '-all' + from { configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } } with jar } + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" +} diff --git a/out/artifacts/Burp_Extractor_jar/Burp_Extractor.jar b/out/artifacts/Burp_Extractor_jar/Burp_Extractor.jar deleted file mode 100644 index c097f2d..0000000 Binary files a/out/artifacts/Burp_Extractor_jar/Burp_Extractor.jar and /dev/null differ diff --git a/out/artifacts/Burp_Extractor_jar/token-extractor-all.jar b/out/artifacts/Burp_Extractor_jar/token-extractor-all.jar deleted file mode 100644 index 9984590..0000000 Binary files a/out/artifacts/Burp_Extractor_jar/token-extractor-all.jar and /dev/null differ diff --git a/src/burp/Extractor.java b/src/burp/Extractor.java deleted file mode 100644 index 2dff321..0000000 --- a/src/burp/Extractor.java +++ /dev/null @@ -1,93 +0,0 @@ -package burp; - -import java.io.PrintWriter; -import java.net.URL; - -public class Extractor implements IHttpListener { - private ExtractorMainTab extractorMainTab; - private IExtensionHelpers helpers; - private Logger logger; - - public Extractor(ExtractorMainTab extractorMainTab, IBurpExtenderCallbacks callbacks) { - this.extractorMainTab = extractorMainTab; - this.helpers = callbacks.getHelpers(); - - this.logger = new Logger(new PrintWriter(callbacks.getStdout(), true)); - Logger.setLogLevel(Logger.INFO); - } - - @Override - public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) { - if (messageIsRequest) { - logger.debug("Processing request..."); - byte[] requestBytes = messageInfo.getRequest(); - String request = this.helpers.bytesToString(requestBytes); - - // Loop over each tab to perform whatever replacement is necessary - String extractedData; - boolean edited = false; - for (ExtractorTab extractorTab : this.extractorMainTab.getExtractorTabs()) { - - // Determine if this message is in scope, and the user wants requests edited at this time - URL url = this.helpers.analyzeRequest(messageInfo.getHttpService(), requestBytes).getUrl(); - if (extractorTab.requestIsInScope(url, - messageInfo.getHttpService().getHost(), - toolFlag) && - extractorTab.shouldModifyRequests()) { - logger.debug("Request is in scope and Extractor tab is active."); - - // Check if we have the necessary components to do replacement - String[] requestSelectionRegex = extractorTab.getRequestSelectionRegex(); - extractedData = extractorTab.getDataToInsert(); - if (!extractedData.equals("") - && !requestSelectionRegex[0].equals("") - && !requestSelectionRegex[1].equals("")) { - logger.debug("Attempting replacement..."); - int[] selectionBounds = Utils.getSelectionBounds(request, requestSelectionRegex[0], requestSelectionRegex[1]); - if (selectionBounds != null) { - logger.debug("Found a match"); - request = request.substring(0, selectionBounds[0]) - + extractedData - + request.substring(selectionBounds[1], request.length()); - edited = true; - logger.debug("Finished replacement"); - } - } - } - } - if (edited) { - messageInfo.setRequest(request.getBytes()); - } - } else if (!messageIsRequest) { - - logger.debug("Processing response..."); - byte[] responseBytes = messageInfo.getResponse(); - String response = this.helpers.bytesToString(responseBytes); - - // Loop over each tab, and grab whatever data item is necessary - for (ExtractorTab extractorTab : this.extractorMainTab.getExtractorTabs()) { - - // Check if message is in scope - URL url = this.helpers.analyzeRequest(messageInfo.getHttpService(), messageInfo.getRequest()).getUrl(); - if (extractorTab.responseIsInScope(url, - messageInfo.getHttpService().getHost(), - toolFlag)) { - logger.debug("Response is in scope."); - - String[] responseSelectionRegex = extractorTab.getResponseSelectionRegex(); - - // Grab text from response - if (responseSelectionRegex[0] != "" && responseSelectionRegex[1] != "") { - int[] selectionBounds = Utils.getSelectionBounds(response, responseSelectionRegex[0], responseSelectionRegex[1]); - if (selectionBounds != null) { - logger.debug("Found a match"); - extractorTab.setDataToInsert(response.substring(selectionBounds[0], selectionBounds[1])); - } - } else { - logger.debug("Before and after regex not defined"); - } - } - } - } - } -} diff --git a/src/burp/BurpExtender.java b/src/main/java/burp/BurpExtender.java similarity index 100% rename from src/burp/BurpExtender.java rename to src/main/java/burp/BurpExtender.java diff --git a/src/burp/ButtonTabComponent.java b/src/main/java/burp/ButtonTabComponent.java similarity index 100% rename from src/burp/ButtonTabComponent.java rename to src/main/java/burp/ButtonTabComponent.java diff --git a/src/main/java/burp/Extractor.java b/src/main/java/burp/Extractor.java new file mode 100644 index 0000000..0a7e9c2 --- /dev/null +++ b/src/main/java/burp/Extractor.java @@ -0,0 +1,131 @@ +package burp; + +import java.io.PrintWriter; +import java.net.URL; + +public class Extractor implements IHttpListener { + private ExtractorMainTab extractorMainTab; + private IExtensionHelpers helpers; + private Logger logger; + + public Extractor(ExtractorMainTab extractorMainTab, IBurpExtenderCallbacks callbacks) { + this.extractorMainTab = extractorMainTab; + this.helpers = callbacks.getHelpers(); + + this.logger = new Logger(new PrintWriter(callbacks.getStdout(), true)); + // Logger.setLogLevel(Logger.INFO); + } + + @Override + public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) { + if (messageIsRequest) { + logger.debug("Processing request..."); + byte[] requestBytes = messageInfo.getRequest(); + String request = this.helpers.bytesToString(requestBytes); + + // Loop over each tab to perform whatever replacement is necessary + String extractedData; + boolean edited = false; + for (ExtractorTab extractorTab : this.extractorMainTab.getExtractorTabs()) { + + // Determine if this message is in scope, and the user wants requests edited at this time + URL url = this.helpers.analyzeRequest(messageInfo.getHttpService(), requestBytes).getUrl(); + if (extractorTab.requestIsInScope(url, + messageInfo.getHttpService().getHost(), + toolFlag) && + extractorTab.shouldModifyRequests()) { + logger.debug("Request is in scope and Extractor tab is active."); + + // Check if we have the necessary components to do replacement + String[] requestSelectionRegex = extractorTab.getRequestSelectionRegex(); + extractedData = extractorTab.getDataToInsert(); + if (!extractedData.equals("") + && !requestSelectionRegex[0].equals("") + && !requestSelectionRegex[1].equals("")) { + logger.debug("Attempting replacement..."); + int[] selectionBounds = Utils.getSelectionBounds(request, requestSelectionRegex[0], requestSelectionRegex[1]); + if (selectionBounds != null) { + logger.info("Replacing request after regex \"" + requestSelectionRegex[0] + "\" with \"" + extractedData + "\""); + int[] clHeaderBounds = Utils.getSelectionBounds(request, "(?i)\\r\\nContent-Length: ", "\\r\\n"); + int[] headersEndBounds = Utils.getSelectionBounds(request, "\\r\\n\\r\\n", ""); + // The following rewrite of the Content-Length + // header aims at maintaining the integrity between + // the header's claim and the rewritten content's + // length. The Content-Length rewrite can still be + // insufficient. For example, the rewrite will not + // fix the MIME parts of a request body that carry + // own content length headers. The Content-Length + // rewrite will not fix the claimed length of a + // chunk in a a chunked Transfer-Encoding. + String dangerousContentLengthRewrite = null; + if ((clHeaderBounds != null) && (headersEndBounds != null) && + (clHeaderBounds[0] < headersEndBounds[0]) && (headersEndBounds[0] < selectionBounds[0])) { + int origContentLength = Integer.parseInt(request.substring(clHeaderBounds[0], + clHeaderBounds[1])); + int replacedLength = selectionBounds[1] - selectionBounds[0]; + int replacedContentLength = origContentLength - replacedLength + extractedData.length(); + if (origContentLength != replacedContentLength) { + logger.info("Updating Content-Length: " + origContentLength + " with " + replacedContentLength); + dangerousContentLengthRewrite = request.substring(0, clHeaderBounds[0]) + + Integer.toString(replacedContentLength) + + request.substring(clHeaderBounds[1], selectionBounds[0]); + } + } + String contentBeforeRewrite; + if (dangerousContentLengthRewrite == null) { + contentBeforeRewrite = request.substring(0, selectionBounds[0]); + } else { + contentBeforeRewrite = dangerousContentLengthRewrite; + } + request = contentBeforeRewrite + + extractedData + + request.substring(selectionBounds[1], request.length()); + edited = true; + logger.debug("Finished replacement"); + } + } + } + } + if (edited) { + messageInfo.setRequest(this.helpers.stringToBytes(request)); + } + } else if (!messageIsRequest) { + + logger.debug("Processing response..."); + byte[] responseBytes = messageInfo.getResponse(); + String response = this.helpers.bytesToString(responseBytes); + + // Loop over each tab, and grab whatever data item is necessary + for (ExtractorTab extractorTab : this.extractorMainTab.getExtractorTabs()) { + + // Check if message is in scope + IHttpService service = messageInfo.getHttpService(); + URL url; + try { + url = new URL(service.getProtocol(), service.getHost(), service.getPort(), ""); + } catch(java.net.MalformedURLException e) { + throw new RuntimeException(e); + } + if (extractorTab.responseIsInScope(url, + service.getHost(), + toolFlag)) { + logger.debug("Response is in scope."); + + String[] responseSelectionRegex = extractorTab.getResponseSelectionRegex(); + + // Grab text from response + if (responseSelectionRegex[0] != "" && responseSelectionRegex[1] != "") { + int[] selectionBounds = Utils.getSelectionBounds(response, responseSelectionRegex[0], responseSelectionRegex[1]); + if (selectionBounds != null) { + logger.info("Found a match in the response after regex \"" + responseSelectionRegex[0] + "\": \"" + + response.substring(selectionBounds[0], selectionBounds[1]) + "\""); + extractorTab.setDataToInsert(response.substring(selectionBounds[0], selectionBounds[1])); + } + } else { + logger.debug("Before and after regex not defined"); + } + } + } + } + } +} diff --git a/src/burp/ExtractorEditor.java b/src/main/java/burp/ExtractorEditor.java similarity index 100% rename from src/burp/ExtractorEditor.java rename to src/main/java/burp/ExtractorEditor.java diff --git a/src/burp/ExtractorMainTab.java b/src/main/java/burp/ExtractorMainTab.java similarity index 98% rename from src/burp/ExtractorMainTab.java rename to src/main/java/burp/ExtractorMainTab.java index 418e719..acb1f0c 100644 --- a/src/burp/ExtractorMainTab.java +++ b/src/main/java/burp/ExtractorMainTab.java @@ -9,7 +9,7 @@ import java.util.HashMap; public class ExtractorMainTab implements ITab { - private HashMap extractorTabMap; + private HashMap extractorTabMap; private ExtractorSelectorTab selectorTab; private int tabNum = 0; static int tabsRemoved = 0; diff --git a/src/burp/ExtractorMenu.java b/src/main/java/burp/ExtractorMenu.java similarity index 100% rename from src/burp/ExtractorMenu.java rename to src/main/java/burp/ExtractorMenu.java diff --git a/src/burp/ExtractorSelectorTab.java b/src/main/java/burp/ExtractorSelectorTab.java similarity index 100% rename from src/burp/ExtractorSelectorTab.java rename to src/main/java/burp/ExtractorSelectorTab.java diff --git a/src/burp/ExtractorTab.java b/src/main/java/burp/ExtractorTab.java similarity index 100% rename from src/burp/ExtractorTab.java rename to src/main/java/burp/ExtractorTab.java diff --git a/src/burp/Logger.java b/src/main/java/burp/Logger.java similarity index 100% rename from src/burp/Logger.java rename to src/main/java/burp/Logger.java diff --git a/src/burp/Utils.java b/src/main/java/burp/Utils.java similarity index 100% rename from src/burp/Utils.java rename to src/main/java/burp/Utils.java diff --git a/src/burp/persistence/ExtractorSetting.java b/src/main/java/burp/persistence/ExtractorSetting.java similarity index 100% rename from src/burp/persistence/ExtractorSetting.java rename to src/main/java/burp/persistence/ExtractorSetting.java diff --git a/src/burp/persistence/ExtractorTabState.java b/src/main/java/burp/persistence/ExtractorTabState.java similarity index 100% rename from src/burp/persistence/ExtractorTabState.java rename to src/main/java/burp/persistence/ExtractorTabState.java diff --git a/src/burp/persistence/InScopeTools.java b/src/main/java/burp/persistence/InScopeTools.java similarity index 100% rename from src/burp/persistence/InScopeTools.java rename to src/main/java/burp/persistence/InScopeTools.java diff --git a/src/burp/persistence/Persistor.java b/src/main/java/burp/persistence/Persistor.java similarity index 100% rename from src/burp/persistence/Persistor.java rename to src/main/java/burp/persistence/Persistor.java diff --git a/src/burp/persistence/RequestResponseState.java b/src/main/java/burp/persistence/RequestResponseState.java similarity index 100% rename from src/burp/persistence/RequestResponseState.java rename to src/main/java/burp/persistence/RequestResponseState.java diff --git a/src/test/java/burp/ExtractorTest.java b/src/test/java/burp/ExtractorTest.java new file mode 100644 index 0000000..52866e4 --- /dev/null +++ b/src/test/java/burp/ExtractorTest.java @@ -0,0 +1,245 @@ +package burp; + +import static org.junit.Assert.assertEquals; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; + +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.UnsupportedEncodingException; +import java.io.ByteArrayOutputStream; +import java.net.URL; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.Arrays; + +@RunWith(MockitoJUnitRunner.class) +public class ExtractorTest { + @Mock + IHttpRequestResponse resp, req; + + @Mock + IHttpService service; + + @Mock + ExtractorMainTab mainTab; + + @Mock + IExtensionHelpers helpers; + + @Mock + IBurpExtenderCallbacks callbacks; + + @Mock + ExtractorTab pathReplacer, tokenReplacer; + + @Mock + IRequestInfo reqInfo; + + @Captor + ArgumentCaptor respCaptor; + + @Captor + ArgumentCaptor reqCaptor; + + String protocol; + String host; + int port; + String hostPort; + String urlStr; + java.net.URL reqUrl; + + String path, detectPathFixture; + String[] rePathResp, rePathReq; + + String token, detectTokenFixture; + String[] reTokenResp, reTokenReq; + int contentLength; + + String respFixture; + byte[] respFixtureBytes; + + String reqFixture; + byte[] reqFixtureBytes; + + ByteArrayOutputStream baos; + + @Before + public void setUp() + throws MalformedURLException, UnsupportedEncodingException { + protocol = "https"; + host = "example.test"; + port = 10081; + hostPort = host + ":" + port; + + detectPathFixture = "/wps/myportal/quux/actions"; + detectTokenFixture = "123456"; + respFixture = "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/html\r\n" + + "\r\n" + + "\r\n" + + "\r\n" + + "\r\n" + + "
\r\n" + + "\r\n"; + respFixtureBytes = respFixture.getBytes("UTF-8"); + + path = "/wps/myportal/foobar/!ut/p/z1/pZXXXXXXXB=EPortletAction!com.YYYY.ZZZZ.actions.SubmitAction==/"; + urlStr = protocol + "://" + hostPort + path; + reqUrl = new java.net.URL(urlStr); + token = "0d5b1d88d9d0a95775781153773dff1c"; + contentLength = 977; + + reqFixture = "POST " + path + " HTTP/1.1\r\n" + + "Host: " + hostPort + "\r\n" + + "Content-Type: application/x-www-form-urlencoded\r\n" + + "Content-Length: " + contentLength + "\r\n" + + "Cookie: quux=baz\r\n" + + "\r\n" + + "test.example.TOKEN=" + token + "¶m=123"; + reqFixtureBytes = reqFixture.getBytes("UTF-8"); + + rePathResp = new String[] {"var portalAction = \"", "\\.SubmitAction==/\""}; + rePathReq = new String[] {"^POST ", "\\.SubmitAction==/"}; + + reTokenResp = new String[] {"\\ bytesToString() { + return new Answer() { + public String answer(InvocationOnMock invocation) { + byte[] ba = (byte[]) (invocation.getArguments()[0]); + StringBuilder sb = new StringBuilder(ba.length); + for(int i = 0; i < ba.length; i++) { + sb.append((char)(ba[i])); + } + return sb.toString(); + } + }; + } + + private static Answer stringToBytes() { + return new Answer() { + public byte[] answer(InvocationOnMock invocation) { + String s = (String) (invocation.getArguments()[0]); + byte[] ba = new byte[s.length()]; + for(int i = 0; i < ba.length; i++) { + ba[i] = (byte)(s.charAt(i)); + } + return ba; + } + }; + } + + private void stub(ExtractorTab[] tabs) { + when(mainTab.getExtractorTabs()).thenReturn(new ArrayList(Arrays.asList(tabs))); + + when(resp.getResponse()).thenReturn(respFixtureBytes); + when(req.getRequest()).thenReturn(reqFixtureBytes); + + when(service.getProtocol()).thenReturn(protocol); + when(service.getHost()).thenReturn(host); + when(service.getPort()).thenReturn(port); + + when(resp.getHttpService()).thenReturn(service); + when(req.getHttpService()).thenReturn(service); + + when(helpers.bytesToString(any(byte[].class))).thenAnswer(bytesToString()); + when(helpers.stringToBytes(any(String.class))).thenAnswer(stringToBytes()); + + when(helpers.analyzeRequest(any(IHttpService.class), any(byte[].class))).thenReturn(reqInfo); + when(reqInfo.getUrl()).thenReturn(reqUrl); + + when(pathReplacer.requestIsInScope(any(java.net.URL.class), any(String.class), anyInt())).thenReturn(true); + when(pathReplacer.responseIsInScope(any(java.net.URL.class), any(String.class), anyInt())).thenReturn(true); + when(pathReplacer.shouldModifyRequests()).thenReturn(true); + when(pathReplacer.getResponseSelectionRegex()).thenReturn(rePathResp); + when(pathReplacer.getRequestSelectionRegex()).thenReturn(rePathReq); + when(pathReplacer.getDataToInsert()).thenReturn(detectPathFixture); + + when(callbacks.getHelpers()).thenReturn(helpers); + when(callbacks.getStdout()).thenReturn(baos); + } + + @Test + public void RewritePathTest() + throws UnsupportedEncodingException { + stub(new ExtractorTab[] { pathReplacer }); + + Extractor extractor = new Extractor(mainTab, callbacks); + + extractor.processHttpMessage(0, false, resp); + + verify(pathReplacer).setDataToInsert(respCaptor.capture()); + assertEquals(detectPathFixture, respCaptor.getValue()); + + extractor.processHttpMessage(0, true, req); + + verify(req).setRequest(reqCaptor.capture()); + + assertEquals(reqFixture + .replace(path, detectPathFixture + ".SubmitAction==/"), + new String(reqCaptor.getValue(), "UTF-8")); + + assertEquals("Found a match in the response after regex \"var portalAction = \"\": \"" + detectPathFixture + "\"\n" + + "Replacing request after regex \"^POST \" with \"" + detectPathFixture + "\"\n", + baos.toString("UTF-8")); + } + + @Test + public void RewritePathAndContentTest() + throws UnsupportedEncodingException { + stub(new ExtractorTab[] { pathReplacer, tokenReplacer }); + + when(tokenReplacer.requestIsInScope(any(java.net.URL.class), any(String.class), anyInt())).thenReturn(true); + when(tokenReplacer.responseIsInScope(any(java.net.URL.class), any(String.class), anyInt())).thenReturn(true); + when(tokenReplacer.shouldModifyRequests()).thenReturn(true); + when(tokenReplacer.getResponseSelectionRegex()).thenReturn(reTokenResp); + when(tokenReplacer.getRequestSelectionRegex()).thenReturn(reTokenReq); + when(tokenReplacer.getDataToInsert()).thenReturn(detectTokenFixture); + + Extractor extractor = new Extractor(mainTab, callbacks); + + extractor.processHttpMessage(0, false, resp); + + verify(pathReplacer).setDataToInsert(respCaptor.capture()); + assertEquals(detectPathFixture, respCaptor.getValue()); + + verify(tokenReplacer).setDataToInsert(respCaptor.capture()); + assertEquals(detectTokenFixture, respCaptor.getValue()); + + extractor.processHttpMessage(0, true, req); + + verify(req).setRequest(reqCaptor.capture()); + + assertEquals(reqFixture + .replace(path, detectPathFixture + ".SubmitAction==/") + .replace(token, detectTokenFixture) + .replace("" + contentLength, "" + (contentLength - token.length() + detectTokenFixture.length())), + new String(reqCaptor.getValue(), "UTF-8")); + + assertEquals("Found a match in the response after regex \"var portalAction = \"\": \"" + detectPathFixture + "\"\n" + + "Found a match in the response after regex \"\\