3 |
4 | # Process all request parameters
5 | # and try to find if they are echoed back in response
6 |
7 | # Version 0.4
8 | # Transformations finally working
9 |
10 | # Version 0.3
11 | # Parsing moved to separate class
12 |
13 | # Version 0.2
14 | # Small fix due to NullPointerException
15 |
16 |
17 | # Burp imports
18 | from burp import IBurpExtender
19 | from burp import IMessageEditorTabFactory
20 | from burp import IMessageEditorTab
21 | from burp import IParameter
22 |
23 | # Java imports
24 | from javax.swing import JTable
25 | from javax.swing import JScrollPane
26 | from javax.swing.table import AbstractTableModel
27 |
28 | # Python imports
29 | from urllib import unquote
30 |
31 | # Consts
32 | MIN_PARAM_LEN = 3
33 |
34 | ## Transformations tables
35 | TRANSFORMATIONS = [
36 | { "name": "plain", "table": []},
37 | { "name": "jinja", "table": [
38 | ('&', '&'),
39 | ('<', '<'),
40 | ('>', '>'),
41 | ('"', '"'),
42 | ('\'', ''')]
43 | },
44 | ]
45 |
46 |
47 | class BurpExtender(IBurpExtender, IMessageEditorTabFactory):
48 | def registerExtenderCallbacks(self, callbacks):
49 | self._callbacks = callbacks
50 | self._helpers = callbacks.getHelpers()
51 |
52 | callbacks.setExtensionName('Argonaut')
53 | callbacks.registerMessageEditorTabFactory(self)
54 |
55 | return
56 |
57 | def createNewInstance(self, controller, editable):
58 | return ArgonautTab(self, controller, editable)
59 |
60 |
61 | class ArgonautTab(IMessageEditorTab):
62 | def __init__(self, extender, controller, editable):
63 | self._extender = extender
64 | self._controller = controller
65 | self._helpers = extender._helpers
66 |
67 | # Data container
68 | self._dataContainer = ArgonautData()
69 |
70 | # Argonaut Parser
71 | self._argoParser = ArgonautParser(self._dataContainer)
72 |
73 | # Burp View (table)
74 | self._argoTable = ArgonautTable(self._dataContainer)
75 | self._tablePane = JScrollPane(self._argoTable)
76 |
77 | return
78 |
79 | def getTabCaption(self):
80 | return "Argonaut"
81 |
82 | def getUiComponent(self):
83 | return self._tablePane
84 |
85 | def isEnabled(self, content, isRequest):
86 | """Enable if parameters were present. Including cookies"""
87 | if isRequest:
88 | return False
89 | else:
90 | raw_req = self._controller.getRequest()
91 |
92 | if not raw_req:
93 | return False
94 |
95 | req = self._helpers.analyzeRequest(raw_req)
96 | params = req.getParameters()
97 |
98 | if params.isEmpty():
99 | return False
100 |
101 | return True
102 |
103 | def setMessage(self, content, isRequest):
104 | if isRequest:
105 | return
106 |
107 | # Extract params from pair
108 | req = self._helpers.analyzeRequest(self._controller.getRequest())
109 | params = req.getParameters()
110 |
111 | # Grab response
112 | rsp = self._helpers.analyzeResponse(content)
113 | body = content[rsp.getBodyOffset():].tostring()
114 |
115 | # Parse
116 | self._dataContainer.reset()
117 | self._argoParser.parse(params, body)
118 | self._dataContainer.fireTableDataChanged()
119 |
120 | return
121 |
122 | def isModified(self):
123 | return False
124 |
125 |
126 | ## ArgonautParser
127 | class ArgonautParser:
128 | def __init__(self, dataContainer):
129 | self.container = dataContainer
130 |
131 | # Transformers to the rescue
132 | self.prime = Optimus()
133 | self.prime.set_transformations(TRANSFORMATIONS)
134 |
135 | def parse(self, params, body):
136 | for param in params:
137 | # TODO: Add different extraction depending on param type
138 | paramValue = unquote(param.getValue())
139 |
140 | # Param testing
141 | if len(paramValue) < MIN_PARAM_LEN: continue
142 |
143 | # Search body
144 | for name, trns in self.prime.transform(paramValue):
145 | indexes = [x for x in list(self.find_all(trns, body))]
146 |
147 | # Extract snippet
148 | if indexes:
149 | for start, end in indexes:
150 | # TODO: more intelligent snippet
151 | snippet = body[max(0,start-30):min(end+30, len(body))]
152 |
153 | self.container.insertRow(paramValue, name, snippet)
154 |
155 | break
156 |
157 | @staticmethod
158 | def find_all(sub, string):
159 | l = len(sub)
160 | start = 0
161 | while True:
162 | start = string.find(sub, start)
163 | if start == -1: return
164 | yield (start, start+l)
165 | start += l
166 |
167 |
168 | ## Transformers class
169 | class Optimus:
170 | def __init__(self):
171 | self.transformations = []
172 |
173 | def set_transformations(self, t):
174 | self.transformations = t
175 |
176 | def transform(self, target):
177 | if target:
178 | for t in self.transformations:
179 | yield t['name'], self._translate(target, t["table"])
180 |
181 | def _translate(self, string, transformation):
182 | return reduce(lambda a, kv: a.replace(*kv), transformation, string)
183 |
184 |
185 | ## Classes related to UITable
186 | # TODO: need serious UI work
187 | class ArgonautTable(JTable):
188 | def __init__(self, dataModel):
189 | self.setModel(dataModel)
190 | return
191 |
192 |
193 | class ArgonautData(AbstractTableModel):
194 | _data = []
195 |
196 | def reset(self):
197 | self._data = []
198 |
199 | def insertRow(self, paramValue, transformation, snippet):
200 | entry = {
201 | 'paramValue': paramValue,
202 | 'transformation': transformation,
203 | 'snippet': snippet
204 | }
205 |
206 | self._data.append(entry)
207 |
208 | def getRowCount(self):
209 | return len(self._data)
210 |
211 | def getColumnCount(self):
212 | return 3
213 |
214 | def getColumnName(self, columnIndex):
215 | if columnIndex == 0:
216 | return "Parameter Value"
217 | if columnIndex == 1:
218 | return "Transformation"
219 | if columnIndex == 2:
220 | return "Snippet"
221 |
222 | return ""
223 |
224 | def getValueAt(self, rowIndex, columnIndex):
225 | dataEntry = self._data[rowIndex]
226 |
227 | if columnIndex == 0:
228 | return dataEntry['paramValue']
229 | if columnIndex == 1:
230 | return dataEntry['transformation']
231 | if columnIndex == 2:
232 | return dataEntry['snippet']
233 |
234 | return ""
235 |
--------------------------------------------------------------------------------
/BappDescription.html:
--------------------------------------------------------------------------------
1 | This extension adds a new tab to Burp's HTTP message editor, and displays JSON messages in decoded form.
2 | This extension requires Jython version 2.7 or later.
--------------------------------------------------------------------------------
/BappManifest.bmf:
--------------------------------------------------------------------------------
1 | Uuid: ceed5b1568ba4b92abecce0dff1e1f2c
2 | ExtensionType: 2
3 | Name: JSON Decoder
4 | RepoName: json-decoder
5 | ScreenVersion: 1.2
6 | SerialVersion: 2
7 | MinPlatformVersion: 0
8 | ProOnly: False
9 | Author: Michal Melewski
10 | ShortDescription: Displays JSON messages in decoded form.
11 | EntryPoint: JSONDecoder.py
12 | BuildCommand: rm -f Argonaut.py
13 |
--------------------------------------------------------------------------------
/JSONDecoder.py:
--------------------------------------------------------------------------------
1 | # Burp Extension - JSON decoder
2 | # Copyright : Michal Melewski
3 |
4 | # Small content-type fix: Nicolas Gregoire
5 | # Force JSON fix: Marcin 'Icewall' Noga
6 |
7 | import json
8 |
9 | from burp import IBurpExtender
10 | from burp import IMessageEditorTabFactory
11 | from burp import IMessageEditorTab
12 | from burp import IParameter
13 | from burp import IContextMenuFactory
14 |
15 | # Java imports
16 | from javax.swing import JMenuItem
17 | from java.util import List, ArrayList
18 |
19 | # Menu items
20 | menuItems = {
21 | False: "Turn JSON active detection on",
22 | True: "Turn JSON active detection off"
23 | }
24 |
25 | # Content types
26 | supportedContentTypes = [
27 | "application/json",
28 | "text/json",
29 | "text/x-json",
30 | ]
31 |
32 | # Global Switch
33 | _forceJSON = False
34 |
35 | class BurpExtender(IBurpExtender, IMessageEditorTabFactory, IContextMenuFactory):
36 | def registerExtenderCallbacks(self, callbacks):
37 | self._callbacks = callbacks
38 | self._helpers = callbacks.getHelpers()
39 |
40 | callbacks.setExtensionName('JSON Decoder')
41 | callbacks.registerMessageEditorTabFactory(self)
42 | callbacks.registerContextMenuFactory(self)
43 |
44 | return
45 |
46 | def createNewInstance(self, controller, editable):
47 | return JSONDecoderTab(self, controller, editable)
48 |
49 | def createMenuItems(self, IContextMenuInvocation):
50 | global _forceJSON
51 | menuItemList = ArrayList()
52 | menuItemList.add(JMenuItem(menuItems[_forceJSON], actionPerformed = self.onClick))
53 |
54 | return menuItemList
55 |
56 | def onClick(self, event):
57 | global _forceJSON
58 | _forceJSON = not _forceJSON
59 |
60 | class JSONDecoderTab(IMessageEditorTab):
61 | def __init__(self, extender, controller, editable):
62 | self._extender = extender
63 | self._helpers = extender._helpers
64 | self._editable = editable
65 |
66 | self._txtInput = extender._callbacks.createTextEditor()
67 | self._txtInput.setEditable(editable)
68 |
69 | self._jsonMagicMark = ['{"', '["', '[{']
70 |
71 | return
72 |
73 | def getTabCaption(self):
74 | return "JSON Decoder"
75 |
76 | def getUiComponent(self):
77 | return self._txtInput.getComponent()
78 |
79 | def isEnabled(self, content, isRequest):
80 | global _forceJSON
81 |
82 | if isRequest:
83 | r = self._helpers.analyzeRequest(content)
84 | else:
85 | r = self._helpers.analyzeResponse(content)
86 |
87 | msg = content[r.getBodyOffset():].tostring()
88 |
89 | if _forceJSON and len(msg) > 2 and msg[:2] in self._jsonMagicMark:
90 | print "Forcing JSON parsing and magic mark found: %s"%msg[:2]
91 | return True
92 |
93 | for header in r.getHeaders():
94 | if header.lower().startswith("content-type:"):
95 | content_type = header.split(":")[1].lower()
96 |
97 | for allowedType in supportedContentTypes:
98 | if content_type.find(allowedType) > 0:
99 | return True
100 |
101 | return False
102 |
103 | def setMessage(self, content, isRequest):
104 | if content is None:
105 | self._txtInput.setText(None)
106 | self._txtInput.setEditable(False)
107 | else:
108 | if isRequest:
109 | r = self._helpers.analyzeRequest(content)
110 | else:
111 | r = self._helpers.analyzeResponse(content)
112 |
113 | msg = content[r.getBodyOffset():].tostring()
114 |
115 | # find garbage index
116 | # I know, this is not bulletproof, but we have to try
117 | try:
118 | boundary = min(
119 | msg.index('{') if '{' in msg else len(msg),
120 | msg.index('[') if '[' in msg else len(msg)
121 | )
122 | except ValueError:
123 | print('Sure this is JSON?')
124 | return
125 |
126 | garbage = msg[:boundary]
127 | clean = msg[boundary:]
128 |
129 | try:
130 | pretty_msg = garbage.strip() + '\n' + json.dumps(json.loads(clean), indent=4)
131 | except:
132 | print "problem parsing data in setMessage"
133 | pretty_msg = garbage + clean
134 |
135 | self._txtInput.setText(pretty_msg)
136 | self._txtInput.setEditable(self._editable)
137 |
138 | self._currentMessage = content
139 | return
140 |
141 | def getMessage(self):
142 | if self._txtInput.isTextModified():
143 | try:
144 | pre_data = self._txtInput.getText().tostring()
145 |
146 | boundary = min(pre_data.index('{'), pre_data.index('['))
147 |
148 | garbage = pre_data[:boundary]
149 | clean = pre_data[boundary:]
150 | data = garbage + json.dumps(json.loads(clean))
151 | except:
152 | data = self._helpers.bytesToString(self._txtInput.getText())
153 |
154 | # Reconstruct request/response
155 | r = self._helpers.analyzeRequest(self._currentMessage)
156 |
157 | return self._helpers.buildHttpMessage(r.getHeaders(), self._helpers.stringToBytes(data))
158 | else:
159 | return self._currentMessage
160 |
161 | def isModified(self):
162 | return self._txtInput.isTextModified()
163 |
164 | def getSelectedData(self):
165 | return self._txtInput.getSelectedText()
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | BurpSuite Extensions
2 | ====================
3 | This is an assorted set of various BurpSuite extensions that I've created while doing pentesting and other things.
4 |
5 | > **Note**
6 | > Every extension is tested on following configuration
7 | >
8 | >- BurpSuite 1.6.01 and newest one (I use free and pro version respectively)
9 | >- Jython 2.7
10 | >
11 | > To send bug reports, feature requests, or whisky, simply drop a mail to michal.melewski@gmail.com
12 |
13 | ### JSONDecoder (1.1)
14 | Quite simply just a JSON pretty printer with some additional features.
15 |
16 | * Ability to remove json garbage (like }]);) - it does a bit of guessing, so not always reliable
17 | * Ability to force JSON decoding on atypical content-type (by default decodes only application/json and text/javascript)
18 |
19 | ### Argonaut (0.4)
20 | Extension process all request parameters and try to find if they are echoed back in response. Key feature is transformation support so, will also recognize if for example > is translated to >. Transformations/escaping currently implemented:
21 | * Plain (no escaping)
22 | * Jinja2 template
23 |
24 | Let me know what other escaping you would like to see
25 |
26 | _Under heavy development so if you want some features let me know.
27 | Also inform me know about bugs._
28 |
--------------------------------------------------------------------------------