diff --git a/.gitignore b/.gitignore index 520879b..8d70ccf 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ releases/ *.iml gradle-app.setting .gradletasknamecache -.git \ No newline at end of file +.git +bin \ No newline at end of file diff --git a/src/main/java/com/coreyd97/stepper/MessageProcessor.java b/src/main/java/com/coreyd97/stepper/MessageProcessor.java index d039891..08894a3 100644 --- a/src/main/java/com/coreyd97/stepper/MessageProcessor.java +++ b/src/main/java/com/coreyd97/stepper/MessageProcessor.java @@ -1,11 +1,15 @@ package com.coreyd97.stepper; import burp.*; + import com.coreyd97.BurpExtenderUtilities.Preferences; import com.coreyd97.stepper.sequence.StepSequence; import com.coreyd97.stepper.sequencemanager.SequenceManager; import com.coreyd97.stepper.util.ReplacingInputStream; +import com.coreyd97.stepper.variable.PreExecutionStepVariable; import com.coreyd97.stepper.variable.StepVariable; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import javax.swing.*; import java.io.ByteArrayInputStream; @@ -22,13 +26,20 @@ public class MessageProcessor implements IHttpListener { private final Preferences preferences; public static final String EXECUTE_BEFORE_HEADER = "X-Stepper-Execute-Before"; public static final String EXECUTE_AFTER_HEADER = "X-Stepper-Execute-After"; + public static final String EXECUTE_VAR_HEADER = "X-Stepper-Argument"; public static final String EXECUTE_BEFORE_REGEX = EXECUTE_BEFORE_HEADER + ":(.*)"; public static final String EXECUTE_AFTER_REGEX = EXECUTE_AFTER_HEADER+":(.*)"; + public static final String EXECUTE_VAR_REGEX = EXECUTE_VAR_HEADER + ":(.*)"; public static final String EXECUTE_AFTER_COMMENT_DELIMITER = "#%~%#"; public static final Pattern EXECUTE_BEFORE_HEADER_PATTERN = Pattern.compile("^" + EXECUTE_BEFORE_REGEX + "$", Pattern.CASE_INSENSITIVE); public static final Pattern EXECUTE_AFTER_HEADER_PATTERN = Pattern.compile("^" + EXECUTE_AFTER_REGEX + "$", Pattern.CASE_INSENSITIVE); + public static final Pattern EXECUTE_VAR_HEADER_PATTERN = Pattern.compile("^" + EXECUTE_VAR_REGEX + "$", Pattern.CASE_INSENSITIVE); public static final String STEPPER_IGNORE_HEADER = "X-Stepper-Ignore"; public static final Pattern STEPPER_IGNORE_PATTERN = Pattern.compile("^"+STEPPER_IGNORE_HEADER, Pattern.CASE_INSENSITIVE); + public static final Pattern STEPPER_SEQUENCE_NAME_PATTERN = Pattern.compile("^([^:]+)(?::?)", Pattern.CASE_INSENSITIVE); + public static final Pattern VARIABLE_LIST_PATTERN = Pattern.compile("[^:]+(:\\s*(?.+))?", Pattern.CASE_INSENSITIVE); + public static final Pattern VARIABLE_PATTERN = Pattern.compile("(?[^=]+)=(?[^;]+);?", Pattern.CASE_INSENSITIVE); + public static final Pattern SINGLE_VARIABLE_PATTERN = Pattern.compile("(?[^=]+)=(?.*)", Pattern.CASE_INSENSITIVE); public MessageProcessor(SequenceManager sequenceManager, Preferences preferences){ this.sequenceManager = sequenceManager; @@ -60,32 +71,58 @@ public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequ if(messageIsRequest){ byte[] request = messageInfo.getRequest(); - System.out.println("Request: " + messageInfo.getRequest()); - List preExecSequences = extractExecSequencesFromRequest(requestInfo, EXECUTE_BEFORE_HEADER_PATTERN); + + // Extract variable headers from request + Map sequenceArguments = extractSequenceArgumentsFromRequest(requestInfo, EXECUTE_VAR_HEADER_PATTERN); + + List preExecSequences = extractExecSequencesFromRequest(requestInfo, EXECUTE_BEFORE_HEADER_PATTERN); if(preExecSequences.size() > 0){ //Remove the headers from the request request = removeHeaderMatchingPattern(request, EXECUTE_BEFORE_HEADER_PATTERN); //Execute the sequences - for (StepSequence sequence : preExecSequences) { - sequence.executeBlocking(); + for (RequestSequenceInformation requestSequenceInformation : preExecSequences) { + StepSequence sequence = requestSequenceInformation.sequence; + Map variables = requestSequenceInformation.variables; + + // Merge arguments from variable headers into arguments for this sequence + // Prioritize arguments specifically set for this sequence + for (Map.Entry entry : sequenceArguments.entrySet()) { + variables.putIfAbsent(entry.getKey(), entry.getValue()); + } + + sequence.executeBlocking(variables); } } - List postExecSequences = extractExecSequencesFromRequest(requestInfo, EXECUTE_AFTER_HEADER_PATTERN); + List postExecSequences = extractExecSequencesFromRequest(requestInfo, EXECUTE_AFTER_HEADER_PATTERN); if(postExecSequences.size() > 0){ //Remove the headers from the request request = removeHeaderMatchingPattern(request, EXECUTE_AFTER_HEADER_PATTERN); + HashMap> allSequences = new HashMap<>(); + //Joining the sequences as string to set them as a comment to catch them in the response String postExecSequencesJoined = EXECUTE_AFTER_HEADER + ":"; - for (StepSequence sequence : postExecSequences) { - postExecSequencesJoined += sequence.getTitle() + EXECUTE_AFTER_COMMENT_DELIMITER; + for (RequestSequenceInformation requestSequenceInformation : postExecSequences) { + StepSequence sequence = requestSequenceInformation.sequence; + Map variables = requestSequenceInformation.variables; + + // Merge arguments from variable headers into arguments for this sequence + // Prioritize arguments specifically set for this sequence + for (Map.Entry entry : sequenceArguments.entrySet()) { + variables.putIfAbsent(entry.getKey(), entry.getValue()); + } + + allSequences.put(sequence.getTitle(), variables); } + Gson gson = new Gson(); + String serializedSequences = gson.toJson(allSequences); + if(!postExecSequencesJoined.isEmpty()){ - messageInfo.setComment(messageInfo.getComment() + postExecSequencesJoined); + messageInfo.setComment(messageInfo.getComment() + EXECUTE_AFTER_HEADER + ":" + serializedSequences); } } @@ -116,11 +153,13 @@ public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequ messageInfo.setRequest(request); }else{ // this is a response so we check for the comments - List postExecSequences = extractExecSequencesFromComment(messageInfo.getComment(), EXECUTE_AFTER_HEADER_PATTERN); + List postExecSequences = extractExecSequencesFromComment(messageInfo.getComment(), EXECUTE_AFTER_HEADER_PATTERN); if(postExecSequences.size() > 0){ //Execute the sequences - for (StepSequence sequence : postExecSequences) { - sequence.executeBlocking(); + for (RequestSequenceInformation requestSequenceInformation : postExecSequences) { + StepSequence sequence = requestSequenceInformation.sequence; + Map variables = requestSequenceInformation.variables; + sequence.executeBlocking(variables); } // remove the added comment from the request messageInfo.setComment(messageInfo.getComment().replaceAll(EXECUTE_AFTER_REGEX+EXECUTE_AFTER_COMMENT_DELIMITER,"")); @@ -225,29 +264,135 @@ public static byte[] updateContentLength(byte[] request){ return Stepper.callbacks.getHelpers().buildHttpMessage(newRequestHeaders, newBody); } + /** + * Extract all defined variables from the request sequence information string. + * + * If a variable has more than one definition, the later one is used. + * It expects the string to have a format that matches the definition in RequesteSequenceInformation. + * + * For example, the string: `test: firstVariable=old; secondVariable=someValue; firstVariable=new` + * would be converted to a map with the following content: + * - firstVariable: new + * - secondVariable: someValue + * + * @see RequestSequenceInformation + * @param sequenceInfoString The string that contains sequence information + * @return All defined variables + */ + public Map extractVariablesFromSequenceInfoString(String sequenceInfoString) { + HashMap map = new HashMap<>(); + + Matcher variableListMatcher = VARIABLE_LIST_PATTERN.matcher(sequenceInfoString); + + if (variableListMatcher.find() && variableListMatcher.group("variables") != null) { + String variableList = variableListMatcher.group("variables"); + Matcher variablesMatcher = VARIABLE_PATTERN.matcher(variableList); + + while (variablesMatcher.find()) { + String variableKey = variablesMatcher.group("key"); + String variableValue = variablesMatcher.group("value"); + + map.put(variableKey, variableValue); + } + } + + return map; + } + + /** + * Extract a sequence's name from the request info string. + * @param sequenceInfoString String that contains the name and variables of a step sequence + * @return The sequence name + */ + private Optional extractSequenceNameFromSequenceInfoString(String sequenceInfoString) { + Matcher nameMatcher = STEPPER_SEQUENCE_NAME_PATTERN.matcher(sequenceInfoString); + if (!nameMatcher.find()) { + return Optional.empty(); + } + + return Optional.of(nameMatcher.group(1).trim()); + } + + /** + * Extract arguments from variable headers from the given request. + * @param requestInfo The request + * @param pattern Pattern used to identify variable header + * @return Map of all sequence arguments + */ + private Map extractSequenceArgumentsFromRequest(IRequestInfo requestInfo, Pattern pattern) { + //Check if headers ask us to execute a request before the request. + List requestHeaders = requestInfo.getHeaders(); + Map arguments = new HashMap<>(); + + for (Iterator iterator = requestHeaders.iterator(); iterator.hasNext(); ) { + String header = iterator.next(); + + Matcher m = pattern.matcher(header); + if (!m.matches()) { + continue; + } + + String variableInfo = m.group(1).trim(); + + + Matcher argumentMatcher = SINGLE_VARIABLE_PATTERN.matcher(variableInfo); + if (!argumentMatcher.matches()) { + continue; + } + + String variableKey = argumentMatcher.group("key"); + String variableValue = argumentMatcher.group("value"); + + arguments.put(variableKey, variableValue); + } + + return arguments; + } + /** * Locates the X-Stepper-Execute-Before or X-Stepper-Execute-After headers and returns the matching sequences. * @param requestInfo * @param pattern * @return Optional value of step sequence to execute before or after the request. */ - public List extractExecSequencesFromRequest(IRequestInfo requestInfo, Pattern pattern){ + public List extractExecSequencesFromRequest(IRequestInfo requestInfo, Pattern pattern){ //Check if headers ask us to execute a request before the request. List requestHeaders = requestInfo.getHeaders(); - ArrayList execSequences = new ArrayList<>(); + ArrayList execSequences = new ArrayList<>(); for (Iterator iterator = requestHeaders.iterator(); iterator.hasNext(); ) { String header = iterator.next(); + Matcher m = pattern.matcher(header); - if (m.matches()) { - Optional execSequence = sequenceManager.getSequences().stream() - .filter(sequence -> sequence.getTitle().equalsIgnoreCase(m.group(1).trim())) - .findFirst(); + if (!m.matches()) { + continue; + } - if(execSequence.isPresent()) - execSequences.add(execSequence.get()); - else - JOptionPane.showMessageDialog(Stepper.getUI().getUiComponent(), "Could not find execution sequence named: \"" + m.group(1).trim() + "\"."); + ArrayList currentSequences = new ArrayList<>(); + String stepperHeader = m.group(1).trim(); + + Optional optionalName = this.extractSequenceNameFromSequenceInfoString(stepperHeader); + + if (optionalName.isEmpty()) { + continue; + } + + String sequenceName = optionalName.get(); + + Optional execSequence = sequenceManager.getSequences().stream() + .filter(sequence -> sequence.getTitle().equalsIgnoreCase(sequenceName)) + .findFirst(); + + if(execSequence.isPresent()) + currentSequences.add(execSequence.get()); + else + JOptionPane.showMessageDialog(Stepper.getUI().getUiComponent(), "Could not find execution sequence named: \"" + sequenceName + "\"."); + + // Extract variables and store them in the sequences + Map arguments = extractVariablesFromSequenceInfoString(stepperHeader); + + for (StepSequence sequence : currentSequences) { + execSequences.add(new RequestSequenceInformation(sequence, arguments)); } } return execSequences; @@ -259,22 +404,28 @@ public List extractExecSequencesFromRequest(IRequestInfo requestIn * @param pattern * @return Optional value of step sequence to execute after the request. */ - public List extractExecSequencesFromComment(String comment, Pattern pattern){ - ArrayList execSequences = new ArrayList<>(); + public List extractExecSequencesFromComment(String comment, Pattern pattern){ + ArrayList execSequences = new ArrayList<>(); Matcher m = pattern.matcher(comment); if (m.find()) { - String[] allSequences = m.group(1).split(EXECUTE_AFTER_COMMENT_DELIMITER); - for(String sequenceName : allSequences){ - if(sequenceName != null && !sequenceName.isEmpty()){ - Optional execSequence = sequenceManager.getSequences().stream() - .filter(sequence -> sequence.getTitle().equalsIgnoreCase(sequenceName.trim())) - .findFirst(); - - if(execSequence.isPresent()) - execSequences.add(execSequence.get()); - else - JOptionPane.showMessageDialog(Stepper.getUI().getUiComponent(), "Could not find execution sequence named: \"" + m.group(1).trim() + "\"."); - } + String allSequencesSerialized = m.group(1); + Gson gson = new Gson(); + TypeToken>> mapType = new TypeToken>>(){}; + HashMap> allSequences = gson.fromJson(allSequencesSerialized, mapType.getType()); + + for(Map.Entry> entry : allSequences.entrySet()){ + String sequenceName = entry.getKey(); + + Optional execSequence = sequenceManager.getSequences().stream() + .filter(sequence -> sequence.getTitle().equalsIgnoreCase(sequenceName)) + .findFirst(); + + Map variables = entry.getValue(); + + if(execSequence.isPresent()) + execSequences.add(new RequestSequenceInformation(execSequence.get(), variables)); + else + JOptionPane.showMessageDialog(Stepper.getUI().getUiComponent(), "Could not find execution sequence named: \"" + sequenceName + "\"."); } } diff --git a/src/main/java/com/coreyd97/stepper/RequestSequenceInformation.java b/src/main/java/com/coreyd97/stepper/RequestSequenceInformation.java new file mode 100644 index 0000000..d4eb8f4 --- /dev/null +++ b/src/main/java/com/coreyd97/stepper/RequestSequenceInformation.java @@ -0,0 +1,43 @@ +package com.coreyd97.stepper; + +import java.util.Map; + +import com.coreyd97.stepper.sequence.StepSequence; + +/** + * Holds information about a step sequence as defined in a request with the stepper headers. + * Includes information about all defined variables. + * + * This information is stored either in the request's X-Stepper-Execute-Before and X-Steper-Execute-After header + * as well in the user comments in the following format: + * : (=;)* + * + * Here some examples to demonstrate how the string representation of this looks like: + *
+ * - `simpleExample: first=value`
+ *  - Name: simpleExample
+ *  - Variables:
+ *      first: value
+ * - `advancedExample: firstKey=firstValue;secondKey=secondValue; firstKey=overwrittenValue;`
+ *  - Name: advancedExample
+ *  - Variables:
+ *      firstKey: overwrittenValue
+ *      secondKey: secondValue
+ * 
+ */ +class RequestSequenceInformation { + /** + * Defines step sequence. + */ + public StepSequence sequence; + + /** + * All variables for the sequence as defined in the stepper header. + */ + public Map variables; + + public RequestSequenceInformation(StepSequence sequence, Map variables) { + this.sequence = sequence; + this.variables = variables; + } +} \ No newline at end of file diff --git a/src/main/java/com/coreyd97/stepper/about/view/AboutPanel.java b/src/main/java/com/coreyd97/stepper/about/view/AboutPanel.java index 952e0fb..bf2386c 100644 --- a/src/main/java/com/coreyd97/stepper/about/view/AboutPanel.java +++ b/src/main/java/com/coreyd97/stepper/about/view/AboutPanel.java @@ -162,7 +162,23 @@ public AboutPanel(){ "as usual, and add the headers "; String stepExecutionUsageItalics = "\"X-Stepper-Execute-Before: SEQUENCENAME\", \"X-Stepper-Execute-After: SEQUENCENAME\""; String stepExecutionUsageB = " to the request respectively. This will cause the sequence to be executed and variables " + - "to be updated every time the request is sent."; + "to be updated every time the request is sent.\n\n"; + + String sequenceInputHeader = "Passing Arguments to Sequences:\n"; + String sequenceInputUsageA = "It is possible to pass arguments to sequences. " + + "This is helpful if the sequences before or after the execution of a request must contain some dynamic value that must match with the request itself. "+ + "In combination with other tools such as intruder or active scanner, this also allows testing more complex sequences and pass the tool's payload into the sequences.\n" + + "Before you can pass arguments, you must first define a global variable for a sequence with your desired name. " + + "The arguments are based on the global variables of a sequence and use the same names. If an argument is passed to the sequence, the value will override the global's value in the sequence. "+ + "There are two ways you can pass an argument.\n\n" + + "1. Option: Execute before and after header:\n" + + "You can pass the arguments in the stepper headers as shown below. You can pass a single argument or multiple arguments at once. The only limitation of this approach is that you can't pass the character \";\" as a value as it is used as the delimiter.\n"; + String sequenceInputUsageBItalics = "\"X-Stepper-Execute-Before: SEQUENCENAME: arg1=value1\" or \"X-Stepper-Execute-After: SEQUENCENAME: arg1=value1; arg2=value2\"\n\n"; + String sequenceInputUsageC = "2. Option: Argument header:\n" + + "If there is the need to pass a semicolon or you are using payloads from another tool where the semicolon might be used, you can use a dedicated header to pass the argument." + + "This header allows you to use any value as an argument but you can only set one argument per header. If you want to pass multiple arguments you have to use multiple headers." + + "You can use this option as shown below:\n"; + String sequenceInputUsageDItalics = "\"X-Stepper-Argument: var=value\""; // aboutContent.getDocument().insertString(aboutContent.getText().length(), intro, italics); // aboutContent.getDocument().insertString(aboutContent.getText().length(), instructionsHeader, bold); @@ -178,13 +194,24 @@ public AboutPanel(){ //Doing this an odd way since insertString seems to cause errors on windows! int offset = 0; - String[] sections = new String[]{intro, instructionsHeader, instructions, variableHelpHeader, variableHelp - , regularExpressionHeader, regularExpressionHelp, regularExpressionExampleHeader - , regularExpressionExample, variableUsageHeader, variableInsertion, variableExampleSequenceTitle, variableExampleSequenceUsage - , variableExampleToolTitle, variableExampleToolUsage, stepExecutionHeader, stepExecutionUsageA, - stepExecutionUsageItalics, stepExecutionUsageB}; - Style[] styles = new Style[]{italics, bold, null, bold, null, bold, null, bold, - null, bold, null, null, italics, null, italics, bold, null, italics, null}; + String[] sections = new String[]{ + intro, + instructionsHeader, instructions, + variableHelpHeader, variableHelp, + regularExpressionHeader, regularExpressionHelp, + regularExpressionExampleHeader, regularExpressionExample, + variableUsageHeader, variableInsertion, variableExampleSequenceTitle, variableExampleSequenceUsage, variableExampleToolTitle, variableExampleToolUsage, + stepExecutionHeader, stepExecutionUsageA, stepExecutionUsageItalics, stepExecutionUsageB, + sequenceInputHeader, sequenceInputUsageA, sequenceInputUsageBItalics, sequenceInputUsageC, sequenceInputUsageDItalics}; + Style[] styles = new Style[]{ + italics, + bold, null, + bold, null, + bold, null, + bold, null, + bold, null, null, italics, null, italics, + bold, null, italics, null, + bold, null, italics, null, italics}; String content = String.join("", sections); aboutContent.setText(content); for (int i = 0; i < sections.length; i++) { diff --git a/src/main/java/com/coreyd97/stepper/sequence/StepSequence.java b/src/main/java/com/coreyd97/stepper/sequence/StepSequence.java index 8895532..06ed5fb 100644 --- a/src/main/java/com/coreyd97/stepper/sequence/StepSequence.java +++ b/src/main/java/com/coreyd97/stepper/sequence/StepSequence.java @@ -17,8 +17,10 @@ import javax.swing.*; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Vector; public class StepSequence @@ -42,11 +44,28 @@ public StepSequence(){ this("Step Sequence"); } - public void executeBlocking(){ + /** + * Execute the step sequence with a predefined list of global variables. + * + * Variables are set in the synchronization block which should ensure that if the sequence + * has already been executed with another set of variable in between the extraction of the + * variables from the header or comment, it is still executed with this set of variables. + * @param newGlobalVariables Map of global variables + */ + public void executeBlocking(Map newGlobalVariables) { if(this.isExecuting) return; //Sequence already being executed. this.isExecuting = true; try { synchronized (StepSequence.this) { + // Assign variables to global variables + for (StepVariable variable : this.globalVariablesManager.getVariables()) { + if (newGlobalVariables.containsKey(variable.getIdentifier())) { + String newValue = newGlobalVariables.get(variable.getIdentifier()); + + variable.setValue(newValue); + } + } + StepSequenceTab tabUI = Stepper.getUI().getTabForStepManager(this); SequenceContainer sequenceContainer = tabUI.getStepsContainer(); @@ -99,6 +118,10 @@ public void executeBlocking(){ } } + public void executeBlocking(){ + this.executeBlocking(new HashMap<>()); + } + public void executeAsync(){ new Thread(() -> executeBlocking()).start(); }