├── .gitignore ├── README.md ├── pom.xml └── src ├── burp └── BurpExtender.java └── main └── java ├── BurpSuiteGraphQLHistory.java ├── ExtensionState.java ├── GraphQLHistoryEvent.java ├── HistoryView.form ├── HistoryView.java ├── MessageEditorController.java └── ProxyListener.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /target/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BurpGraphQLViewer 2 | This extension provides a central location for viewing all GraphQL requests/responses within a Burp project. It provides a clean UI that groups all requests by "operationName" and for each GraphQL request shows a pretty printed view of the query and the raw Burp Suite Request/Response. 3 | 4 | # Example UI 5 | ![ExtensionUI](https://user-images.githubusercontent.com/16274749/155235135-b4f8bdb2-9e97-4d9b-a388-ed2890e9da6a.PNG) 6 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.staticflow 8 | BurpGraphQLHistory 9 | 1.0-SNAPSHOT 10 | 11 | 12 | net.portswigger.burp.extender 13 | burp-extender-api 14 | 2.3 15 | 16 | 17 | org.apache.commons 18 | commons-lang3 19 | 3.12.0 20 | 21 | 22 | com.graphql-java 23 | graphql-java 24 | 17.3 25 | 26 | 27 | 28 | 8 29 | 8 30 | 31 | 32 | ${project.basedir}/src 33 | BurpSuiteGraphQLHistory 34 | 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-eclipse-plugin 40 | 2.9 41 | 42 | true 43 | false 44 | 45 | 46 | 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-compiler-plugin 51 | 2.3.2 52 | 53 | 1.8 54 | 1.8 55 | 56 | 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-assembly-plugin 62 | 2.4.1 63 | 64 | 65 | false 66 | 67 | jar-with-dependencies 68 | 69 | 70 | 71 | 72 | 73 | make-assembly 74 | 75 | package 76 | 77 | single 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/burp/BurpExtender.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import main.java.BurpSuiteGraphQLHistory; 4 | 5 | //See @BurpSuiteGraphQLHistory for extension functionality 6 | public class BurpExtender extends BurpSuiteGraphQLHistory { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/BurpSuiteGraphQLHistory.java: -------------------------------------------------------------------------------- 1 | package main.java; 2 | 3 | import burp.*; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.nio.charset.StandardCharsets; 8 | 9 | /* 10 | This extension collects all previous and future GraphQL requests and displays them in a separate tab for easy viewing. 11 | The graphql requests are grouped by Operation and each request for that operation is shown in a detailed view to the side 12 | which contains the pretty printed GraphQL request, along with the request/response tabs. 13 | */ 14 | public class BurpSuiteGraphQLHistory implements IBurpExtender, 15 | IExtensionStateListener, ITab { 16 | @Override 17 | public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { 18 | ExtensionState.getInstance().setCallbacks(callbacks); 19 | ExtensionState.getInstance().setHistoryUI(new HistoryView()); 20 | callbacks.registerExtensionStateListener(this); 21 | callbacks.addSuiteTab(this); 22 | callbacks.setExtensionName("BurpSuiteGraphQLHistory"); 23 | callbacks.registerProxyListener(ExtensionState.getInstance().getProxyListener()); 24 | new Thread(this::searchExistingHistoryForGraphQLRequests).start(); 25 | 26 | } 27 | 28 | //Searches the Target History map for any GraphQL requests that happened before the extension was loaded 29 | private void searchExistingHistoryForGraphQLRequests() { 30 | for(IHttpRequestResponse request : ExtensionState.getInstance().getCallbacks().getProxyHistory() ) { 31 | ExtensionState.getInstance().parseRequestForGraphQLContent(request); 32 | } 33 | } 34 | 35 | //Removes the proxy listener and extension tab when the extension is removed 36 | @Override 37 | public void extensionUnloaded() { 38 | ExtensionState.getInstance().getCallbacks().removeProxyListener(ExtensionState.getInstance().getProxyListener()); 39 | ExtensionState.getInstance().getCallbacks().removeSuiteTab(this); 40 | } 41 | 42 | @Override 43 | public String getTabCaption() { 44 | return "GraphQL History"; 45 | } 46 | 47 | @Override 48 | public Component getUiComponent() { 49 | return ExtensionState.getInstance().getHistoryUI(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/ExtensionState.java: -------------------------------------------------------------------------------- 1 | package main.java; 2 | 3 | import burp.IBurpExtenderCallbacks; 4 | import burp.IHttpRequestResponse; 5 | import burp.IParameter; 6 | import burp.IRequestInfo; 7 | import graphql.language.Field; 8 | import graphql.language.OperationDefinition; 9 | import graphql.parser.Parser; 10 | 11 | import javax.swing.*; 12 | import java.awt.*; 13 | import java.util.*; 14 | 15 | 16 | /* 17 | Static singleton class for keeping extension state. 18 | */ 19 | public class ExtensionState { 20 | 21 | //reference to this state object 22 | private static ExtensionState state = null; 23 | //reference to the Burp callbacks 24 | private IBurpExtenderCallbacks callbacks; 25 | //map of all GraphQL requests, keys are "Operations", values are the request data 26 | private final HashMap> graphQLHistoryEvents; 27 | //reference to the Burp Proxy Listener that catches GraphQL requests 28 | private final ProxyListener httpProxyListener; 29 | //reference to this extensions custom Tab UI 30 | private HistoryView historyUI; 31 | 32 | 33 | private ExtensionState() { 34 | graphQLHistoryEvents = new HashMap<>(); 35 | httpProxyListener = new ProxyListener(); 36 | 37 | } 38 | 39 | public static ExtensionState getInstance() { 40 | if (state == null) 41 | state = new ExtensionState(); 42 | return state; 43 | } 44 | 45 | public IBurpExtenderCallbacks getCallbacks() { 46 | return callbacks; 47 | } 48 | 49 | public void setCallbacks(IBurpExtenderCallbacks callbacks) { 50 | this.callbacks = callbacks; 51 | } 52 | 53 | public Map> getGraphQLHistoryEventsMap() { 54 | return graphQLHistoryEvents; 55 | } 56 | 57 | /* 58 | Inserts a new GraphQL requests into the map of current GraphQL requests 59 | */ 60 | public void addEventToGraphQLHistoryEventsMap(GraphQLHistoryEvent event, String operationName) { 61 | //if we have seen this operation before append the new event, else create a new entry with the new event 62 | ArrayList operationEvents = graphQLHistoryEvents.computeIfAbsent(operationName, k -> new ArrayList<>()); 63 | operationEvents.add(event); 64 | if( operationEvents.size() == 1) { 65 | new SwingWorker() { 66 | @Override 67 | public Boolean doInBackground() { 68 | ((DefaultListModel)historyUI.getOperationsListModel()).addElement(operationName); 69 | return Boolean.TRUE; 70 | } 71 | }.execute(); 72 | } else { 73 | historyUI.updateOperationsTable(operationName); 74 | } 75 | } 76 | 77 | public ProxyListener getProxyListener() { 78 | return httpProxyListener; 79 | } 80 | 81 | public Component getHistoryUI() { 82 | return historyUI.$$$getRootComponent$$$(); 83 | } 84 | 85 | public void setHistoryUI(HistoryView historyUI) { 86 | this.historyUI = historyUI; 87 | } 88 | 89 | /* 90 | This method extracts the operation name from the GraphQL query body by first checking is the "operationName" field is set and if not, uses the first 91 | statement selection as the operation name. 92 | */ 93 | private String parseGraphQLGetRequest(String query) { 94 | OperationDefinition operation; 95 | try { 96 | //Attempt to parse the GraphQL query 97 | operation = (OperationDefinition) (new Parser().parseDocument(query).getDefinitions().get(0)); 98 | //check if "operationName" is not set, get the name of the first selection 99 | if (operation.getName() == null || Objects.equals(operation.getName(), "")) { 100 | return ((Field) operation.getSelectionSet().getSelections().get(0)).getName(); 101 | } else { 102 | //return the "operationName" 103 | return operation.getName(); 104 | } 105 | } catch (Exception e) { 106 | getCallbacks().printError(e.getMessage()); 107 | return ""; 108 | } 109 | } 110 | 111 | /* 112 | Parse potential Proxy Request to determine if it's a GraphQL request following the steps below: 113 | 1. Check if the URL contains the string "graphql" (This may be lossy or over-zealous) There's no universal GraphQL endpoint scheme 114 | 2. Try and extract the query and operationName from the request. 115 | 3. Parse the query if the "operationName" is not set. 116 | 4. If an "operationName" was found, add it to the GraphQL history map. 117 | */ 118 | public void parseRequestForGraphQLContent(IHttpRequestResponse proxyRequestResponse) { 119 | IRequestInfo requestInfo = ExtensionState.getInstance().getCallbacks().getHelpers().analyzeRequest(proxyRequestResponse); 120 | //If url for request is a graphql endpoint 121 | if (requestInfo.getUrl().toString().contains("graphql")) { 122 | String operationName = ""; 123 | String query = ""; 124 | //Try and extract our parameters if they exist 125 | for (IParameter p : requestInfo.getParameters()) { 126 | switch (p.getName()) { 127 | case "query": 128 | query = getCallbacks().getHelpers().urlDecode(p.getValue()).replaceAll("\\\\n",""); 129 | break; 130 | case "operationName": 131 | operationName = p.getValue(); 132 | break; 133 | default: 134 | //ignore any other query params 135 | break; 136 | } 137 | } 138 | 139 | if (query.length() == 0) { 140 | //If the query is empty this is probably not a GraphQL request. 141 | return; 142 | } 143 | 144 | //Burp parses the JSON literal `null` as the string "null" so we have to check for that here 145 | if (operationName.length() == 0 || operationName.equals("null")) { 146 | //If the operationName isn't provided the query is parsed to find it 147 | operationName = parseGraphQLGetRequest(query); 148 | } 149 | 150 | if( operationName.length() != 0 ) { 151 | addEventToGraphQLHistoryEventsMap( 152 | new GraphQLHistoryEvent( 153 | proxyRequestResponse, 154 | query 155 | ), 156 | operationName 157 | ); 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/GraphQLHistoryEvent.java: -------------------------------------------------------------------------------- 1 | package main.java; 2 | 3 | import burp.IHttpRequestResponse; 4 | import burp.IMessageEditor; 5 | import graphql.language.AstPrinter; 6 | import graphql.parser.Parser; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | 11 | 12 | /* 13 | This class represents a GraphQL request that was found in the Target History or detected via the Proxy Listener 14 | */ 15 | public class GraphQLHistoryEvent { 16 | //The Burp request/response pair 17 | private final IHttpRequestResponse graphQLQueryRequestResponse; 18 | //The extracted GraphQL query 19 | private final String graphQLQuery; 20 | //The detailed view component for viewing this GraphQL Event 21 | private final Component detailedView; 22 | 23 | 24 | public GraphQLHistoryEvent(IHttpRequestResponse graphQLQueryRequestResponse, String graphQLQuery) { 25 | this.graphQLQueryRequestResponse = graphQLQueryRequestResponse; 26 | this.graphQLQuery = graphQLQuery; 27 | this.detailedView = buildDetailedView(); 28 | } 29 | 30 | /* 31 | The event detailed view consists of a pretty printed text area containing the GraphQL query and the Request/Response Burp view like in Repeater tabs. 32 | */ 33 | private Component buildDetailedView() { 34 | JSplitPane operationDetailView = new JSplitPane(); 35 | operationDetailView.setDividerLocation(0.5); 36 | JTextArea graphQLQueryText = new JTextArea(AstPrinter.printAst(new Parser().parseDocument(graphQLQuery))); 37 | graphQLQueryText.setEditable(false); 38 | operationDetailView.setLeftComponent(graphQLQueryText); 39 | JTabbedPane requestResponseEditors = new JTabbedPane(); 40 | IMessageEditor requestEditor = ExtensionState.getInstance().getCallbacks().createMessageEditor( 41 | new MessageEditorController( 42 | graphQLQueryRequestResponse.getHttpService(), 43 | graphQLQueryRequestResponse.getRequest(), 44 | graphQLQueryRequestResponse.getResponse() 45 | ), 46 | true); 47 | requestEditor.setMessage(graphQLQueryRequestResponse.getRequest(),true); 48 | requestResponseEditors.addTab("Request",requestEditor.getComponent()); 49 | IMessageEditor responseEditor = ExtensionState.getInstance().getCallbacks().createMessageEditor( 50 | new MessageEditorController( 51 | graphQLQueryRequestResponse.getHttpService(), 52 | graphQLQueryRequestResponse.getRequest(), 53 | graphQLQueryRequestResponse.getResponse() 54 | ), 55 | true); 56 | responseEditor.setMessage(graphQLQueryRequestResponse.getResponse(),false); 57 | requestResponseEditors.addTab("Response",responseEditor.getComponent()); 58 | operationDetailView.setRightComponent(requestResponseEditors); 59 | return operationDetailView; 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "GraphQLHistoryEvent{" + 65 | ", graphQLQueryRequestResponse=" + graphQLQueryRequestResponse + 66 | ", graphQLQuery='" + graphQLQuery + '\'' + 67 | '}'; 68 | } 69 | 70 | public Component getDetailedView() { 71 | return detailedView; 72 | } 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/java/HistoryView.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | -------------------------------------------------------------------------------- /src/main/java/HistoryView.java: -------------------------------------------------------------------------------- 1 | package main.java; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.util.ArrayList; 6 | 7 | /* 8 | This is generated from Jetbrains Intellij SwingDesigner. It sets up the UI code for displaying the GraphQLHistoryEvent Model. 9 | */ 10 | public class HistoryView { 11 | private JPanel basePanel; 12 | private JList operationsList; 13 | private JPanel operationsPanel; 14 | private JScrollPane operationsListScroller; 15 | private JPanel operationsVewPanel; 16 | private JTabbedPane operationsTable; 17 | 18 | public ListModel getOperationsListModel() { 19 | return operationsList.getModel(); 20 | } 21 | 22 | private void createUIComponents() { 23 | operationsList = new JList<>(); 24 | operationsList.setModel(new DefaultListModel<>()); 25 | operationsList.addListSelectionListener(e -> updateInViewOperationTable()); 26 | } 27 | 28 | public void updateOperationsTable(String operationName) { 29 | if (operationsList.getSelectedIndex() != -1 && operationsList.getModel().getElementAt(operationsList.getSelectedIndex()).equals(operationName)) { 30 | updateInViewOperationTable(); 31 | } 32 | } 33 | 34 | private void updateInViewOperationTable() { 35 | new SwingWorker() { 36 | @Override 37 | public Boolean doInBackground() { 38 | operationsTable.removeAll(); 39 | ArrayList selectedHistoryEventsByOperation = ExtensionState.getInstance().getGraphQLHistoryEventsMap().get(operationsList.getModel().getElementAt(operationsList.getSelectedIndex())); 40 | for (int i = 0; i < selectedHistoryEventsByOperation.size(); i++) { 41 | operationsTable.addTab(String.valueOf(i), selectedHistoryEventsByOperation.get(i).getDetailedView()); 42 | } 43 | return Boolean.TRUE; 44 | } 45 | }.execute(); 46 | } 47 | 48 | 49 | { 50 | // GUI initializer generated by IntelliJ IDEA GUI Designer 51 | // >>> IMPORTANT!! <<< 52 | // DO NOT EDIT OR ADD ANY CODE HERE! 53 | $$$setupUI$$$(); 54 | } 55 | 56 | /** 57 | * Method generated by IntelliJ IDEA GUI Designer 58 | * >>> IMPORTANT!! <<< 59 | * DO NOT edit this method OR call it in your code! 60 | * 61 | * @noinspection ALL 62 | */ 63 | private void $$$setupUI$$$() { 64 | createUIComponents(); 65 | basePanel = new JPanel(); 66 | basePanel.setLayout(new BorderLayout(0, 0)); 67 | operationsPanel = new JPanel(); 68 | operationsPanel.setLayout(new BorderLayout(0, 0)); 69 | basePanel.add(operationsPanel, BorderLayout.WEST); 70 | operationsListScroller = new JScrollPane(); 71 | operationsPanel.add(operationsListScroller, BorderLayout.CENTER); 72 | operationsList.setMinimumSize(new Dimension(50, 0)); 73 | operationsList.setSelectionMode(0); 74 | operationsListScroller.setViewportView(operationsList); 75 | operationsVewPanel = new JPanel(); 76 | operationsVewPanel.setLayout(new BorderLayout(0, 0)); 77 | basePanel.add(operationsVewPanel, BorderLayout.CENTER); 78 | operationsTable = new JTabbedPane(); 79 | operationsVewPanel.add(operationsTable, BorderLayout.CENTER); 80 | } 81 | 82 | /** 83 | * @noinspection ALL 84 | */ 85 | public JComponent $$$getRootComponent$$$() { 86 | return basePanel; 87 | } 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/MessageEditorController.java: -------------------------------------------------------------------------------- 1 | package main.java; 2 | 3 | import burp.IHttpService; 4 | import burp.IMessageEditorController; 5 | 6 | //This stub is needed for enabling "right click send to Repeater/Intruder/etc" from within the GraphQLHistoryEvent detailed view 7 | public class MessageEditorController implements IMessageEditorController { 8 | 9 | private final IHttpService service; 10 | private final byte[] request; 11 | private final byte[] response; 12 | 13 | public MessageEditorController(IHttpService service, byte[] request, byte[] response) { 14 | this.service = service; 15 | this.request = request; 16 | this.response = response; 17 | } 18 | 19 | @Override 20 | public IHttpService getHttpService() { 21 | return service; 22 | } 23 | 24 | @Override 25 | public byte[] getRequest() { 26 | return request; 27 | } 28 | 29 | @Override 30 | public byte[] getResponse() { 31 | return response; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/ProxyListener.java: -------------------------------------------------------------------------------- 1 | package main.java; 2 | 3 | import burp.IHttpRequestResponse; 4 | import burp.IInterceptedProxyMessage; 5 | import burp.IProxyListener; 6 | 7 | /* 8 | This class listens to all Proxy traffic and parses responses for whether it is in response to a GraphQL request. 9 | */ 10 | public class ProxyListener implements IProxyListener { 11 | 12 | @Override 13 | public void processProxyMessage(boolean messageIsRequest, IInterceptedProxyMessage proxyMessage) { 14 | if (!messageIsRequest) { 15 | IHttpRequestResponse proxyRequestResponse = proxyMessage.getMessageInfo(); 16 | ExtensionState.getInstance().parseRequestForGraphQLContent(proxyRequestResponse); 17 | } 18 | } 19 | 20 | 21 | } 22 | --------------------------------------------------------------------------------