1 | | - | # |
2 | | - | # BurpLinkFinder - Find links within JS files. |
3 | | - | # |
4 | | - | # Copyright (c) 2019 Frans Hendrik Botes |
5 | | - | # Credit to https://github.com/GerbenJavado/LinkFinder for the idea and regex |
6 | | - | # |
7 | | - | from burp import IBurpExtender, IScannerCheck, IScanIssue, ITab |
8 | | - | from java.io import PrintWriter |
9 | | - | from java.net import URL |
10 | | - | from java.util import ArrayList, List |
11 | | - | from java.util.regex import Matcher, Pattern |
12 | | - | import binascii |
13 | | - | import base64 |
14 | | - | import re |
15 | | - | from javax import swing |
16 | | - | from java.awt import Font, Color |
17 | | - | from threading import Thread |
18 | | - | from array import array |
19 | | - | from java.awt import EventQueue |
20 | | - | from java.lang import Runnable |
21 | | - | from multiprocessing.dummy import Pool as ThreadPool |
22 | | - | |
23 | | - | # Using the Runnable class for thread-safety with Swing |
24 | | - | class Run(Runnable): |
25 | | - | def __init__(self, runner): |
26 | | - | self.runner = runner |
27 | | - | |
28 | | - | def run(self): |
29 | | - | self.runner() |
30 | | - | |
31 | | - | |
32 | | - | class BurpExtender(IBurpExtender, IScannerCheck, ITab): |
33 | | - | def registerExtenderCallbacks(self, callbacks): |
34 | | - | self.callbacks = callbacks |
35 | | - | self.helpers = callbacks.getHelpers() |
36 | | - | callbacks.setExtensionName("BurpJSLinkFinder") |
37 | | - | |
38 | | - | callbacks.issueAlert("BurpJSLinkFinder Passive Scanner enabled") |
39 | | - | |
40 | | - | stdout = PrintWriter(callbacks.getStdout(), True) |
41 | | - | stderr = PrintWriter(callbacks.getStderr(), True) |
42 | | - | callbacks.registerScannerCheck(self) |
43 | | - | self.initUI() |
44 | | - | self.callbacks.addSuiteTab(self) |
45 | | - | |
46 | | - | print ("Burp JS LinkFinder loaded.") |
47 | | - | print ("Copyright (c) 2019 Frans Hendrik Botes") |
48 | | - | self.outputTxtArea.setText("Burp JS LinkFinder loaded." + "\n" + "Copyright (c) 2019 Frans Hendrik Botes" + "\n") |
49 | | - | |
50 | | - | def initUI(self): |
51 | | - | self.tab = swing.JPanel() |
52 | | - | |
53 | | - | # UI for Output |
54 | | - | self.outputLabel = swing.JLabel("LinkFinder Log:") |
55 | | - | self.outputLabel.setFont(Font("Tahoma", Font.BOLD, 14)) |
56 | | - | self.outputLabel.setForeground(Color(255,102,52)) |
57 | | - | self.logPane = swing.JScrollPane() |
58 | | - | self.outputTxtArea = swing.JTextArea() |
59 | | - | self.outputTxtArea.setFont(Font("Consolas", Font.PLAIN, 12)) |
60 | | - | self.outputTxtArea.setLineWrap(True) |
61 | | - | self.logPane.setViewportView(self.outputTxtArea) |
62 | | - | self.clearBtn = swing.JButton("Clear Log", actionPerformed=self.clearLog) |
63 | | - | |
64 | | - | # Layout |
65 | | - | layout = swing.GroupLayout(self.tab) |
66 | | - | layout.setAutoCreateGaps(True) |
67 | | - | layout.setAutoCreateContainerGaps(True) |
68 | | - | self.tab.setLayout(layout) |
69 | | - | |
70 | | - | layout.setHorizontalGroup( |
71 | | - | layout.createParallelGroup() |
72 | | - | .addGroup(layout.createSequentialGroup() |
73 | | - | .addGroup(layout.createParallelGroup() |
74 | | - | .addComponent(self.outputLabel) |
75 | | - | .addComponent(self.logPane) |
76 | | - | .addComponent(self.clearBtn) |
77 | | - | ) |
78 | | - | ) |
79 | | - | ) |
80 | | - | |
81 | | - | layout.setVerticalGroup( |
82 | | - | layout.createParallelGroup() |
83 | | - | .addGroup(layout.createParallelGroup() |
84 | | - | .addGroup(layout.createSequentialGroup() |
85 | | - | .addComponent(self.outputLabel) |
86 | | - | .addComponent(self.logPane) |
87 | | - | .addComponent(self.clearBtn) |
88 | | - | ) |
89 | | - | ) |
90 | | - | ) |
91 | | - | |
92 | | - | def getTabCaption(self): |
93 | | - | return "BurpJSLinkFinder" |
94 | | - | |
95 | | - | def getUiComponent(self): |
96 | | - | return self.tab |
97 | | - | |
98 | | - | def clearLog(self, event): |
99 | | - | self.outputTxtArea.setText("Burp JS LinkFinder loaded." + "\n" + "Copyright (c) 2019 Frans Hendrik Botes" + "\n" ) |
100 | | - | |
101 | | - | def doPassiveScan(self, ihrr): |
102 | | - | try: |
103 | | - | url = ihrr.getUrl() |
104 | | - | linkA = linkAnalyse(ihrr,self.helpers) |
105 | | - | # make the Pool of workers |
106 | | - | pool = ThreadPool(4) |
107 | | - | if ".js" in str(url): |
108 | | - | self.outputTxtArea.append("\n" + "[+] Valid URL found: " + str(url)) |
109 | | - | issueText = pool.map(linkA.analyseURL()) |
110 | | - | for counter, issueText in enumerate(issueText): |
111 | | - | #print("TEST Value returned SUCCESS") |
112 | | - | self.outputTxtArea.append("\n" + "\t" + str(counter)+' - ' +issueText['link']) |
113 | | - | |
114 | | - | issues = ArrayList() |
115 | | - | issues.add(SRI(ihrr, self.helpers)) |
116 | | - | pool.close() |
117 | | - | pool.join() |
118 | | - | return issues |
119 | | - | except UnicodeEncodeError: |
120 | | - | print ("Error in URL decode.") |
121 | | - | return None |
122 | | - | |
123 | | - | |
124 | | - | def consolidateDuplicateIssues(self, isb, isa): |
125 | | - | return -1 |
126 | | - | |
127 | | - | def extensionUnloaded(self): |
128 | | - | print "Burp JS LinkFinder unloaded" |
129 | | - | return |
130 | | - | |
131 | | - | class linkAnalyse(): |
132 | | - | |
133 | | - | def __init__(self, reqres, helpers): |
134 | | - | self.helpers = helpers |
135 | | - | self.reqres = reqres |
136 | | - | |
137 | | - | |
138 | | - | regex_str = """ |
139 | | - | |
140 | | - | (?:"|') # Start newline delimiter |
141 | | - | |
142 | | - | ( |
143 | | - | ((?:[a-zA-Z]{1,10}://|//) # Match a scheme [a-Z]*1-10 or // |
144 | | - | [^"'/]{1,}\. # Match a domainname (any character + dot) |
145 | | - | [a-zA-Z]{2,}[^"']{0,}) # The domainextension and/or path |
146 | | - | |
147 | | - | | |
148 | | - | |
149 | | - | ((?:/|\.\./|\./) # Start with /,../,./ |
150 | | - | [^"'><,;| *()(%%$^/\\\[\]] # Next character can't be... |
151 | | - | [^"'><,;|()]{1,}) # Rest of the characters can't be |
152 | | - | |
153 | | - | | |
154 | | - | |
155 | | - | ([a-zA-Z0-9_\-/]{1,}/ # Relative endpoint with / |
156 | | - | [a-zA-Z0-9_\-/]{1,} # Resource name |
157 | | - | \.(?:[a-zA-Z]{1,4}|action) # Rest + extension (length 1-4 or action) |
158 | | - | (?:[\?|/][^"|']{0,}|)) # ? mark with parameters |
159 | | - | |
160 | | - | | |
161 | | - | |
162 | | - | ([a-zA-Z0-9_\-]{1,} # filename |
163 | | - | \.(?:php|asp|aspx|jsp|json| |
164 | | - | action|html|js|txt|xml) # . + extension |
165 | | - | (?:\?[^"|']{0,}|)) # ? mark with parameters |
166 | | - | |
167 | | - | ) |
168 | | - | |
169 | | - | (?:"|') # End newline delimiter |
170 | | - | |
171 | | - | """ |
172 | | - | |
173 | | - | def parser_file(self, content, regex_str, mode=1, more_regex=None, no_dup=1): |
174 | | - | #print ("TEST parselfile #2") |
175 | | - | regex = re.compile(regex_str, re.VERBOSE) |
176 | | - | items = [{"link": m.group(1)} for m in re.finditer(regex, content)] |
177 | | - | if no_dup: |
178 | | - | # Remove duplication |
179 | | - | all_links = set() |
180 | | - | no_dup_items = [] |
181 | | - | for item in items: |
182 | | - | if item["link"] not in all_links: |
183 | | - | all_links.add(item["link"]) |
184 | | - | no_dup_items.append(item) |
185 | | - | items = no_dup_items |
186 | | - | |
187 | | - | # Match Regex |
188 | | - | filtered_items = [] |
189 | | - | for item in items: |
190 | | - | # Remove other capture groups from regex results |
191 | | - | if more_regex: |
192 | | - | if re.search(more_regex, item["link"]): |
193 | | - | #print ("TEST parselfile #3") |
194 | | - | filtered_items.append(item) |
195 | | - | else: |
196 | | - | filtered_items.append(item) |
197 | | - | return filtered_items |
198 | | - | |
199 | | - | def threadAnalysis(self): |
200 | | - | thread = Thread(target=self.analyseURL(), args=(session,)) |
201 | | - | thread.daemon = True |
202 | | - | thread.start() |
203 | | - | |
204 | | - | def analyseURL(self): |
205 | | - | |
206 | | - | endpoints = "" |
207 | | - | #print("TEST AnalyseURL #1") |
208 | | - | mime_type=self.helpers.analyzeResponse(self.reqres.getResponse()).getStatedMimeType() |
209 | | - | if mime_type.lower() == 'script': |
210 | | - | url = self.reqres.getUrl() |
211 | | - | encoded_resp=binascii.b2a_base64(self.reqres.getResponse()) |
212 | | - | decoded_resp=base64.b64decode(encoded_resp) |
213 | | - | endpoints=self.parser_file(decoded_resp, self.regex_str) |
214 | | - | #print("TEST AnalyseURL #2") |
215 | | - | return endpoints |
216 | | - | return endpoints |
217 | | - | |
218 | | - | |
219 | | - | class SRI(IScanIssue,ITab): |
220 | | - | def __init__(self, reqres, helpers): |
221 | | - | self.helpers = helpers |
222 | | - | self.reqres = reqres |
223 | | - | |
224 | | - | def getHost(self): |
225 | | - | return self.reqres.getHost() |
226 | | - | |
227 | | - | def getPort(self): |
228 | | - | return self.reqres.getPort() |
229 | | - | |
230 | | - | def getProtocol(self): |
231 | | - | return self.reqres.getProtocol() |
232 | | - | |
233 | | - | def getUrl(self): |
234 | | - | return self.reqres.getUrl() |
235 | | - | |
236 | | - | def getIssueName(self): |
237 | | - | return "Linkfinder Analysed JS files" |
238 | | - | |
239 | | - | def getIssueType(self): |
240 | | - | return 0x08000000 # See http:#portswigger.net/burp/help/scanner_issuetypes.html |
241 | | - | |
242 | | - | def getSeverity(self): |
243 | | - | return "Information" # "High", "Medium", "Low", "Information" or "False positive" |
244 | | - | |
245 | | - | def getConfidence(self): |
246 | | - | return "Certain" # "Certain", "Firm" or "Tentative" |
247 | | - | |
248 | | - | def getIssueBackground(self): |
249 | | - | return str("JS files holds links to other parts of web applications. Refer to TAB for results.") |
250 | | - | |
251 | | - | def getRemediationBackground(self): |
252 | | - | return "This is an <b>informational</b> finding only.<br>" |
253 | | - | |
254 | | - | def getIssueDetail(self): |
255 | | - | return str("Burp Scanner has analysed the following JS file for links: <b>" |
256 | | - | "%s</b><br><br>" % (self.reqres.getUrl().toString())) |
257 | | - | |
258 | | - | def getRemediationDetail(self): |
259 | | - | return None |
260 | | - | |
261 | | - | def getHttpMessages(self): |
262 | | - | #print ("................raising issue................") |
263 | | - | rra = [self.reqres] |
264 | | - | return rra |
265 | | - | |
266 | | - | def getHttpService(self): |
267 | | - | return self.reqres.getHttpService() |
268 | | - | |
269 | | - | |
270 | | - | |
271 | | - | if __name__ in ('__main__', 'main'): |
272 | | - | EventQueue.invokeLater(Run(BurpExtender)) |
273 | | - | |